Spaces:
Running
Running
File size: 11,772 Bytes
af62db2 85bb85c af62db2 85bb85c af62db2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | -- ============================================================
-- 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,
customer_tier TEXT NOT NULL DEFAULT 'free'
CHECK (customer_tier IN ('free','starter','growth','enterprise','vip')),
channel TEXT NOT NULL DEFAULT 'api'
CHECK (channel IN ('email','chat','api','phone','portal')),
raw_text TEXT NOT NULL, -- original ticket body (stored encrypted)
masked_text TEXT,
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;
|