-- ===================================================================== -- ACADFLOW CRM SAAS MULTI-TENANT & SECURITY MIGRATION (V2) -- ===================================================================== -- Run this script in your Supabase SQL Editor to transition your database -- to a production-grade multi-tenant SaaS schema. BEGIN; -- 1. Create Tenant Organization Table CREATE TABLE IF NOT EXISTS organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, plan_type VARCHAR(50) DEFAULT 'Starter', -- e.g., 'Starter', 'Growth', 'Enterprise' created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 2. Create Branch Table CREATE TABLE IF NOT EXISTS branches ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, location VARCHAR(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- 3. Insert Default Tenant & Branch to prevent breaking existing data INSERT INTO organizations (id, name, plan_type) VALUES ('00000000-0000-0000-0000-000000000001', 'Default Academy', 'Starter') ON CONFLICT (id) DO NOTHING; INSERT INTO branches (id, organization_id, name, location) VALUES ('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000001', 'Primary Branch', 'Main Office') ON CONFLICT (id) DO NOTHING; -- 4. Alter Counselors Table ALTER TABLE counselors ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL; ALTER TABLE counselors ADD COLUMN IF NOT EXISTS branch_id UUID REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE counselors ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE; -- Update existing counselors to default tenant UPDATE counselors SET organization_id = '00000000-0000-0000-0000-000000000001' WHERE organization_id IS NULL; UPDATE counselors SET branch_id = '00000000-0000-0000-0000-000000000002' WHERE branch_id IS NULL; -- 5. Alter Leads Table ALTER TABLE leads ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL; ALTER TABLE leads ADD COLUMN IF NOT EXISTS branch_id UUID REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE leads ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE; -- Update existing leads to default tenant UPDATE leads SET organization_id = '00000000-0000-0000-0000-000000000001' WHERE organization_id IS NULL; UPDATE leads SET branch_id = '00000000-0000-0000-0000-000000000002' WHERE branch_id IS NULL; -- 6. Alter FollowUps Table ALTER TABLE follow_ups ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL; ALTER TABLE follow_ups ADD COLUMN IF NOT EXISTS branch_id UUID REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE follow_ups ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE; -- Update existing follow-ups to default tenant UPDATE follow_ups SET organization_id = '00000000-0000-0000-0000-000000000001' WHERE organization_id IS NULL; UPDATE follow_ups SET branch_id = '00000000-0000-0000-0000-000000000002' WHERE branch_id IS NULL; -- 7. Alter Admissions Table ALTER TABLE admissions ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL; ALTER TABLE admissions ADD COLUMN IF NOT EXISTS branch_id UUID REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE admissions ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE; -- Update existing admissions to default tenant UPDATE admissions SET organization_id = '00000000-0000-0000-0000-000000000001' WHERE organization_id IS NULL; UPDATE admissions SET branch_id = '00000000-0000-0000-0000-000000000002' WHERE branch_id IS NULL; -- 8. Alter AI Insights Table ALTER TABLE ai_insights ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL; ALTER TABLE ai_insights ADD COLUMN IF NOT EXISTS branch_id UUID REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE ai_insights ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP WITH TIME ZONE; -- Update existing insights to default tenant UPDATE ai_insights SET organization_id = '00000000-0000-0000-0000-000000000001' WHERE organization_id IS NULL; UPDATE ai_insights SET branch_id = '00000000-0000-0000-0000-000000000002' WHERE branch_id IS NULL; -- 9. Apply NOT NULL constraints after data updates to keep db strict ALTER TABLE counselors ALTER COLUMN organization_id SET NOT NULL; ALTER TABLE leads ALTER COLUMN organization_id SET NOT NULL; ALTER TABLE follow_ups ALTER COLUMN organization_id SET NOT NULL; ALTER TABLE admissions ALTER COLUMN organization_id SET NOT NULL; ALTER TABLE ai_insights ALTER COLUMN organization_id SET NOT NULL; -- 10. Create Active Views to auto-filter soft-deleted rows in application reads CREATE OR REPLACE VIEW active_counselors AS SELECT * FROM counselors WHERE deleted_at IS NULL; CREATE OR REPLACE VIEW active_leads AS SELECT * FROM leads WHERE deleted_at IS NULL; CREATE OR REPLACE VIEW active_follow_ups AS SELECT * FROM follow_ups WHERE deleted_at IS NULL; CREATE OR REPLACE VIEW active_admissions AS SELECT * FROM admissions WHERE deleted_at IS NULL; CREATE OR REPLACE VIEW active_ai_insights AS SELECT * FROM ai_insights WHERE deleted_at IS NULL; -- 11. Row-Level Security (RLS) Policies -- Turn on RLS on tables ALTER TABLE counselors ENABLE ROW LEVEL SECURITY; ALTER TABLE leads ENABLE ROW LEVEL SECURITY; ALTER TABLE follow_ups ENABLE ROW LEVEL SECURITY; ALTER TABLE admissions ENABLE ROW LEVEL SECURITY; ALTER TABLE ai_insights ENABLE ROW LEVEL SECURITY; -- Dynamic Tenant Organization Access Policies (Isolate data strictly by tenant UUID in user metadata JWT claim) CREATE POLICY org_counselor_isolation ON counselors FOR ALL TO authenticated USING (organization_id = (auth.jwt() -> 'user_metadata' ->> 'organization_id')::uuid); CREATE POLICY org_lead_isolation ON leads FOR ALL TO authenticated USING (organization_id = (auth.jwt() -> 'user_metadata' ->> 'organization_id')::uuid); CREATE POLICY org_followup_isolation ON follow_ups FOR ALL TO authenticated USING (organization_id = (auth.jwt() -> 'user_metadata' ->> 'organization_id')::uuid); CREATE POLICY org_admission_isolation ON admissions FOR ALL TO authenticated USING (organization_id = (auth.jwt() -> 'user_metadata' ->> 'organization_id')::uuid); CREATE POLICY org_insight_isolation ON ai_insights FOR ALL TO authenticated USING (organization_id = (auth.jwt() -> 'user_metadata' ->> 'organization_id')::uuid); COMMIT;