summaryrefslogtreecommitdiff
path: root/internal/controlplane/schema.sql
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-06 09:29:41 +1200
committerChia <Chia@93.nz>2026-08-06 09:32:46 +1200
commit41e322c53d7b4b796eb377d0df9c29ecd10ba431 (patch)
treec730526150e55e39b822d5197e4a20318ecaa449 /internal/controlplane/schema.sql
parenteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (diff)
feat: complete commercial control plane, billing, auth, and model catalog
- add PostgreSQL control-plane persistence with Redis-degraded hot reload - implement prepaid balance, usage ledger, Stripe top-up and reconciliation - add registration, email verification, password reset, invitations and RBAC - support TOTP, Passkey MFA, device sessions, quotas and rate limits - add tenant billing profiles, audit logs and operational readiness checks - build authenticated admin console, Quickstart, Playground and usage analytics - add public model catalog with pricing, filtering and cost estimation - support OpenAI Responses providers and provider health failover - validate real upstream usage reporting and balance settlement
Diffstat (limited to '')
-rw-r--r--internal/controlplane/schema.sql110
1 files changed, 110 insertions, 0 deletions
diff --git a/internal/controlplane/schema.sql b/internal/controlplane/schema.sql
index 88db49b..ef1ccdd 100644
--- a/internal/controlplane/schema.sql
+++ b/internal/controlplane/schema.sql
@@ -49,17 +49,53 @@ CREATE TABLE IF NOT EXISTS api_keys (
revoked_at TIMESTAMPTZ,
FOREIGN KEY (project_id, tenant_id) REFERENCES projects(id, tenant_id) ON DELETE CASCADE
);
+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS monthly_spend_micros BIGINT NOT NULL DEFAULT 0 CHECK (monthly_spend_micros >= 0);
+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS tags JSONB NOT NULL DEFAULT '[]'::jsonb;
CREATE TABLE IF NOT EXISTS providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$'),
name TEXT NOT NULL UNIQUE,
protocol TEXT NOT NULL CHECK (protocol IN ('openai', 'anthropic')),
+ wire_api TEXT NOT NULL DEFAULT 'chat_completions' CHECK (wire_api IN ('chat_completions', 'responses', 'messages')),
base_url TEXT NOT NULL,
api_key_ciphertext BYTEA NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
+ALTER TABLE providers ADD COLUMN IF NOT EXISTS slug TEXT;
+WITH normalized AS (
+ SELECT id,
+ left(trim(both '-' from regexp_replace(lower(name), '[^a-z0-9]+', '-', 'g')), 64) AS base
+ FROM providers
+), ranked AS (
+ SELECT id, base, count(*) OVER (PARTITION BY base) AS base_count
+ FROM normalized
+)
+UPDATE providers p
+SET slug = CASE
+ WHEN length(r.base) BETWEEN 3 AND 64
+ AND r.base ~ '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$'
+ AND r.base_count = 1 THEN r.base
+ ELSE 'provider-' || left(replace(p.id::text, '-', ''), 12)
+END
+FROM ranked r
+WHERE p.id = r.id AND (p.slug IS NULL OR p.slug = '');
+ALTER TABLE providers ALTER COLUMN slug SET NOT NULL;
+CREATE UNIQUE INDEX IF NOT EXISTS providers_slug_unique_idx ON providers (slug);
+ALTER TABLE providers DROP CONSTRAINT IF EXISTS providers_slug_check;
+ALTER TABLE providers ADD CONSTRAINT providers_slug_check CHECK (slug ~ '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$');
+ALTER TABLE providers ADD COLUMN IF NOT EXISTS wire_api TEXT NOT NULL DEFAULT 'chat_completions';
+UPDATE providers SET wire_api = 'messages' WHERE protocol = 'anthropic' AND wire_api = 'chat_completions';
+ALTER TABLE providers DROP CONSTRAINT IF EXISTS providers_wire_api_check;
+ALTER TABLE providers ADD CONSTRAINT providers_wire_api_check CHECK (wire_api IN ('chat_completions', 'responses', 'messages'));
+ALTER TABLE providers DROP CONSTRAINT IF EXISTS providers_protocol_wire_api_check;
+ALTER TABLE providers ADD CONSTRAINT providers_protocol_wire_api_check CHECK (
+ (protocol = 'openai' AND wire_api IN ('chat_completions', 'responses')) OR
+ (protocol = 'anthropic' AND wire_api = 'messages')
+);
CREATE TABLE IF NOT EXISTS models (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -145,6 +181,15 @@ CREATE TABLE IF NOT EXISTS api_key_model_allowlist (
PRIMARY KEY (api_key_id, model_id)
);
+-- Per-key restrictions are separate from the platform model allowlist above:
+-- an empty set means that the key may use every model visible to its tenant.
+CREATE TABLE IF NOT EXISTS api_key_model_restrictions (
+ api_key_id UUID NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
+ model_id UUID NOT NULL REFERENCES models(id) ON DELETE CASCADE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (api_key_id, model_id)
+);
+
CREATE TABLE IF NOT EXISTS model_routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
model_id UUID NOT NULL REFERENCES models(id) ON DELETE CASCADE,
@@ -171,6 +216,18 @@ CREATE TABLE IF NOT EXISTS tenant_wallets (
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
+-- Tenant-scoped developer and billing preferences. These values are control
+-- plane data, but are intentionally not loaded into the inference snapshot.
+CREATE TABLE IF NOT EXISTS tenant_preferences (
+ tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
+ default_model TEXT REFERENCES models(public_id) ON DELETE SET NULL,
+ fallback_model TEXT REFERENCES models(public_id) ON DELETE SET NULL,
+ low_balance_enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ low_balance_threshold_micros BIGINT NOT NULL DEFAULT 5000000 CHECK (low_balance_threshold_micros >= 0),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ CHECK (default_model IS NULL OR fallback_model IS NULL OR default_model <> fallback_model)
+);
+
CREATE TABLE IF NOT EXISTS billing_reservations (
request_id TEXT PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
@@ -286,6 +343,10 @@ CREATE TABLE IF NOT EXISTS topup_orders (
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
paid_at TIMESTAMPTZ
);
+ALTER TABLE topup_orders ADD COLUMN IF NOT EXISTS trigger_type TEXT NOT NULL DEFAULT 'manual';
+ALTER TABLE topup_orders DROP CONSTRAINT IF EXISTS topup_orders_trigger_type_check;
+ALTER TABLE topup_orders ADD CONSTRAINT topup_orders_trigger_type_check
+ CHECK (trigger_type IN ('manual','auto'));
ALTER TABLE topup_orders DROP CONSTRAINT IF EXISTS topup_orders_status_check;
ALTER TABLE topup_orders ADD CONSTRAINT topup_orders_status_check
CHECK (status IN ('pending','paid','failed','expired','partially_refunded','refunded','disputed','reversed'));
@@ -306,6 +367,8 @@ ALTER TABLE topup_orders ADD CONSTRAINT topup_orders_reconciliation_status_check
CHECK (reconciliation_status IN ('unknown','ok','repaired','missing','mismatch','resolved'));
CREATE INDEX IF NOT EXISTS topup_orders_payment_intent_idx ON topup_orders (stripe_payment_intent_id) WHERE stripe_payment_intent_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS topup_orders_customer_idx ON topup_orders (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
+CREATE UNIQUE INDEX IF NOT EXISTS topup_orders_auto_pending_idx ON topup_orders (tenant_id)
+ WHERE trigger_type='auto' AND status='pending';
CREATE TABLE IF NOT EXISTS billing_reconciliation_resolutions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -325,6 +388,51 @@ CREATE TABLE IF NOT EXISTS stripe_customers (
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
+CREATE TABLE IF NOT EXISTS tenant_billing_profiles (
+ tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
+ legal_name TEXT NOT NULL,
+ billing_email TEXT NOT NULL,
+ address_line1 TEXT NOT NULL,
+ address_line2 TEXT NOT NULL DEFAULT '',
+ city TEXT NOT NULL,
+ region TEXT NOT NULL DEFAULT '',
+ postal_code TEXT NOT NULL,
+ country TEXT NOT NULL CHECK (country ~ '^[A-Z]{2}$'),
+ stripe_sync_status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (stripe_sync_status IN ('pending','synced','failed','disabled')),
+ stripe_synced_at TIMESTAMPTZ,
+ stripe_sync_error TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE IF NOT EXISTS tenant_auto_topup_settings (
+ tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
+ enabled BOOLEAN NOT NULL DEFAULT FALSE,
+ threshold_micros BIGINT NOT NULL CHECK (threshold_micros >= 0),
+ topup_amount_minor BIGINT NOT NULL CHECK (topup_amount_minor > 0),
+ stripe_payment_method_id TEXT,
+ payment_method_type TEXT NOT NULL DEFAULT '',
+ payment_method_brand TEXT NOT NULL DEFAULT '',
+ payment_method_last4 TEXT NOT NULL DEFAULT '',
+ payment_method_exp_month INTEGER NOT NULL DEFAULT 0 CHECK (payment_method_exp_month BETWEEN 0 AND 12),
+ payment_method_exp_year INTEGER NOT NULL DEFAULT 0 CHECK (payment_method_exp_year >= 0),
+ stripe_setup_session_id TEXT UNIQUE,
+ status TEXT NOT NULL DEFAULT 'not_configured'
+ CHECK (status IN ('not_configured','ready','charging','action_required','failed')),
+ last_error TEXT NOT NULL DEFAULT '',
+ failure_count INTEGER NOT NULL DEFAULT 0 CHECK (failure_count >= 0),
+ last_attempt_at TIMESTAMPTZ,
+ last_succeeded_at TIMESTAMPTZ,
+ next_attempt_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ CHECK (stripe_payment_method_id IS NOT NULL OR enabled = FALSE)
+);
+CREATE INDEX IF NOT EXISTS tenant_auto_topup_ready_idx
+ ON tenant_auto_topup_settings (next_attempt_at, tenant_id)
+ WHERE enabled AND stripe_payment_method_id IS NOT NULL;
+
CREATE TABLE IF NOT EXISTS stripe_refunds (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
@@ -658,8 +766,10 @@ CREATE INDEX IF NOT EXISTS billing_ledger_tenant_idx ON billing_ledger (tenant_i
CREATE INDEX IF NOT EXISTS usage_events_tenant_idx ON usage_events (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS usage_events_project_idx ON usage_events (project_id, created_at DESC);
CREATE INDEX IF NOT EXISTS usage_events_model_idx ON usage_events (public_model, created_at DESC);
+CREATE INDEX IF NOT EXISTS usage_events_key_idx ON usage_events (key_id, started_at DESC);
CREATE INDEX IF NOT EXISTS billing_reservations_pending_idx ON billing_reservations (status, created_at) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS billing_reservations_project_pending_idx ON billing_reservations (project_id, created_at) WHERE status = 'pending';
+CREATE INDEX IF NOT EXISTS billing_reservations_key_period_idx ON billing_reservations (key_id, created_at DESC) WHERE status IN ('pending', 'metering_failed');
CREATE INDEX IF NOT EXISTS console_users_tenant_idx ON console_users (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS console_sessions_user_idx ON console_sessions (user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS console_sessions_expiry_idx ON console_sessions (expires_at) WHERE revoked_at IS NULL;