Spaces:
Running
Running
| -- ===================================================================== | |
| -- 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; | |