Spaces:
Running
Running
| -- ============================================================ | |
| -- CustomerCore β Phase 12: Supabase Schema + Row Level Security | |
| -- Run this in: Supabase Dashboard β SQL Editor β New Query | |
| -- ============================================================ | |
| -- Enable pgcrypto for UUID generation | |
| CREATE EXTENSION IF NOT EXISTS "pgcrypto"; | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 1. TENANTS | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS tenants ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| slug TEXT UNIQUE NOT NULL, -- e.g. 'acme-corp' | |
| name TEXT NOT NULL, | |
| tier TEXT NOT NULL DEFAULT 'growth' | |
| CHECK (tier IN ('free','growth','enterprise','vip')), | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| COMMENT ON TABLE tenants IS 'One row per B2B customer organisation using CustomerCore.'; | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 2. TICKETS | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS tickets ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, | |
| external_ticket_id TEXT, -- caller-supplied ID (idempotency) | |
| customer_id TEXT NOT NULL, | |
| channel TEXT NOT NULL DEFAULT 'api' | |
| CHECK (channel IN ('email','chat','api','phone','portal')), | |
| raw_text TEXT NOT NULL, -- original ticket body (stored encrypted) | |
| detected_language TEXT, | |
| status TEXT NOT NULL DEFAULT 'pending' | |
| CHECK (status IN ('pending','processing','complete','hitl','failed')), | |
| priority TEXT CHECK (priority IN ('critical','high','medium','low')), | |
| category TEXT, | |
| sub_category TEXT, | |
| sentiment TEXT CHECK (sentiment IN ('angry','frustrated','neutral','satisfied','positive')), | |
| churn_risk_score NUMERIC(4,3), -- 0.000β1.000 | |
| constitutional_score NUMERIC(4,3), -- 0.000β1.000 | |
| constitutional_passed BOOLEAN DEFAULT TRUE, | |
| hitl_required BOOLEAN DEFAULT FALSE, | |
| hitl_reason TEXT, | |
| hitl_resolved_at TIMESTAMPTZ, | |
| hitl_resolved_by TEXT, | |
| suggested_resolution TEXT, | |
| resolution_quality NUMERIC(4,3), | |
| rag_grounding_score NUMERIC(4,3), | |
| llm_model_used TEXT, | |
| llm_cost_usd NUMERIC(10,6), | |
| llm_tokens_total INTEGER, | |
| langfuse_trace_id TEXT, -- link back to Langfuse | |
| error_message TEXT, | |
| processing_started_at TIMESTAMPTZ, | |
| processing_completed_at TIMESTAMPTZ, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| CONSTRAINT uq_tenant_external UNIQUE (tenant_id, external_ticket_id) | |
| ); | |
| COMMENT ON TABLE tickets IS 'Persistent store for all triage requests and their AI-generated outcomes.'; | |
| CREATE INDEX IF NOT EXISTS idx_tickets_tenant_status ON tickets (tenant_id, status); | |
| CREATE INDEX IF NOT EXISTS idx_tickets_tenant_created ON tickets (tenant_id, created_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets (priority) WHERE priority IS NOT NULL; | |
| CREATE INDEX IF NOT EXISTS idx_tickets_hitl ON tickets (hitl_required) WHERE hitl_required = TRUE; | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 3. CONSTITUTIONAL VIOLATIONS LOG | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS constitutional_violations ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, | |
| tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, | |
| rule_id TEXT NOT NULL, -- e.g. 'PII_PROTECTION' | |
| severity TEXT NOT NULL, -- 'critical' | 'violation' | 'warning' | |
| action_taken TEXT NOT NULL, -- 'block' | 'redact' | 'regenerate' | 'warn' | |
| evidence TEXT, -- the triggering text (max 200 chars) | |
| explanation TEXT, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| COMMENT ON TABLE constitutional_violations IS 'Audit log: every constitutional rule violation with evidence and remediation.'; | |
| CREATE INDEX IF NOT EXISTS idx_violations_tenant ON constitutional_violations (tenant_id, created_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_violations_rule ON constitutional_violations (rule_id); | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 4. KB ARTICLES (for RAG grounding) | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS kb_articles ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, | |
| external_id TEXT NOT NULL, -- e.g. 'KB-billing-001' | |
| title TEXT NOT NULL, | |
| content TEXT NOT NULL, | |
| category TEXT, | |
| tags TEXT[], | |
| active BOOLEAN NOT NULL DEFAULT TRUE, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| CONSTRAINT uq_kb_tenant_external UNIQUE (tenant_id, external_id) | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_kb_tenant_active ON kb_articles (tenant_id, active); | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 5. AUDIT LOG (immutable β no UPDATE/DELETE) | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS audit_log ( | |
| id BIGSERIAL PRIMARY KEY, | |
| tenant_id UUID REFERENCES tenants(id), | |
| ticket_id UUID REFERENCES tickets(id), | |
| actor TEXT, -- user_id, 'system', 'ai' | |
| action TEXT NOT NULL, -- 'ticket.created', 'hitl.approved', etc. | |
| resource_type TEXT, | |
| resource_id TEXT, | |
| details JSONB, | |
| ip_address INET, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| COMMENT ON TABLE audit_log IS 'Append-only audit trail. No DELETE/UPDATE permitted (enforced by RLS).'; | |
| CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit_log (tenant_id, created_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_audit_ticket ON audit_log (ticket_id); | |
| CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 6. ROW LEVEL SECURITY | |
| -- Tenants can ONLY see their own rows. This is enforced at the | |
| -- database level β even a buggy application cannot leak data. | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Enable RLS on every tenant-scoped table | |
| ALTER TABLE tickets ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE constitutional_violations ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE kb_articles ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY; | |
| -- RLS policy: service role bypasses (for backend API) | |
| -- Application uses SUPABASE_SERVICE_ROLE_KEY β full access | |
| -- Public anon key β restricted by these policies | |
| -- tickets: tenant isolation | |
| CREATE POLICY tenant_isolation_tickets ON tickets | |
| USING (tenant_id::text = current_setting('app.tenant_id', TRUE)); | |
| -- constitutional_violations: tenant isolation | |
| CREATE POLICY tenant_isolation_violations ON constitutional_violations | |
| USING (tenant_id::text = current_setting('app.tenant_id', TRUE)); | |
| -- kb_articles: tenant isolation (read their own KB, not others') | |
| CREATE POLICY tenant_isolation_kb ON kb_articles | |
| USING (tenant_id::text = current_setting('app.tenant_id', TRUE)); | |
| -- audit_log: tenant can read own entries, cannot write directly | |
| CREATE POLICY tenant_read_audit ON audit_log | |
| FOR SELECT | |
| USING (tenant_id::text = current_setting('app.tenant_id', TRUE)); | |
| -- Prevent any direct INSERT/UPDATE/DELETE on audit_log from non-service roles | |
| CREATE POLICY audit_log_insert_service_only ON audit_log | |
| FOR INSERT | |
| WITH CHECK (current_setting('role', TRUE) = 'service_role'); | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 7. auto-update updated_at trigger | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE OR REPLACE FUNCTION update_updated_at() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| NEW.updated_at = now(); | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| CREATE TRIGGER trg_tickets_updated_at | |
| BEFORE UPDATE ON tickets | |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); | |
| CREATE TRIGGER trg_tenants_updated_at | |
| BEFORE UPDATE ON tenants | |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); | |
| CREATE TRIGGER trg_kb_updated_at | |
| BEFORE UPDATE ON kb_articles | |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- 8. Seed: demo tenant | |
| -- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| INSERT INTO tenants (id, slug, name, tier) | |
| VALUES ( | |
| 'a0000000-0000-0000-0000-000000000001', | |
| 'acme-corp', | |
| 'Acme Corporation', | |
| 'enterprise' | |
| ) ON CONFLICT (slug) DO NOTHING; | |