veylant/migrations/000001_initial.up.sql
2026-02-23 13:35:04 +01:00

103 lines
5.0 KiB
PL/PgSQL

-- Migration 000001: Initial schema
-- Tables: tenants, users, api_keys
-- Row Level Security enabled for logical multi-tenancy (physical isolation in V2)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================
-- TENANTS
-- ============================================================
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE, -- URL-safe identifier, e.g. "acme-corp"
plan TEXT NOT NULL DEFAULT 'starter' CHECK (plan IN ('starter', 'business', 'enterprise')),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tenants_slug ON tenants(slug);
-- ============================================================
-- USERS
-- ============================================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
external_id TEXT NOT NULL, -- Keycloak subject (sub claim)
email TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'manager', 'user', 'auditor')),
department TEXT, -- e.g. "finance", "legal", "engineering"
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, external_id),
UNIQUE (tenant_id, email)
);
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_users_external_id ON users(external_id);
CREATE INDEX idx_users_email ON users(tenant_id, email);
-- ============================================================
-- API KEYS
-- Keys are stored as SHA-256 hashes — never in plaintext.
-- The first 8 chars are kept as prefix for identification (e.g. "sk-vyl_ab12cd34...").
-- ============================================================
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL, -- Human-readable label, e.g. "Finance dept key"
key_hash TEXT NOT NULL UNIQUE, -- SHA-256(raw_key), hex-encoded
key_prefix TEXT NOT NULL, -- First 8 chars of raw key for display
provider TEXT NOT NULL CHECK (provider IN ('openai', 'anthropic', 'azure', 'mistral', 'ollama')),
scopes TEXT[] NOT NULL DEFAULT '{}', -- e.g. {'chat:completions', 'embeddings'}
is_active BOOLEAN NOT NULL DEFAULT TRUE,
expires_at TIMESTAMPTZ, -- NULL = no expiry
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_api_keys_tenant_id ON api_keys(tenant_id);
CREATE INDEX idx_api_keys_key_hash ON api_keys(key_hash);
-- ============================================================
-- ROW LEVEL SECURITY
-- App connects as role 'veylant_app' and sets app.tenant_id per session.
-- Policies are added here for completeness; the role is created in env setup.
-- ============================================================
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
-- Superuser bypasses RLS — dev connections use superuser, prod connections use veylant_app role.
-- Policies below apply only to veylant_app role (added in env-specific setup scripts).
-- ============================================================
-- updated_at auto-update trigger
-- ============================================================
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tenants_updated_at BEFORE UPDATE ON tenants FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TRIGGER api_keys_updated_at BEFORE UPDATE ON api_keys FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- ============================================================
-- Seed: default development tenant and admin user
-- (Development only — replaced by real onboarding in prod)
-- ============================================================
INSERT INTO tenants (id, name, slug, plan) VALUES
('00000000-0000-0000-0000-000000000001', 'Veylant Dev', 'veylant-dev', 'enterprise');
INSERT INTO users (tenant_id, external_id, email, role, department) VALUES
('00000000-0000-0000-0000-000000000001', 'dev-admin', 'admin@veylant.dev', 'admin', 'engineering');