-- 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');