Migration Sequence Document
Overview
This document outlines the detailed step-by-step migration order for implementing multitenancy. The sequence is designed to ensure zero downtime and data integrity.
Critical Principles
- Add nullable columns first - Allows gradual migration without breaking existing code
- Backfill before constraints - Ensure all data has tenant_id before making it NOT NULL
- RLS changes after backfill - Policies can safely reference tenant_id after data is populated
- Application changes after RLS - Frontend must be ready to provide tenant_id when RLS enforces it
- Tighten constraints last - Make tenant_id NOT NULL and add tenant-scoped uniques only after everything else is working
Phase 1: Schema Foundation (Add Nullable Columns)
Step 1.1: Create tenants table
Migration File: *_14_create_tenants_table.sql
CREATE TABLE IF NOT EXISTS tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
created_at timestamptz DEFAULT now()
);
Rollback: DROP TABLE IF EXISTS tenants CASCADE;
Step 1.2: Add tenant_id to profiles
Migration File: *_15_add_tenant_id_columns.sql (part 1)
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id);
CREATE INDEX IF NOT EXISTS idx_profiles_tenant_id ON profiles(tenant_id);
Rollback:
DROP INDEX IF EXISTS idx_profiles_tenant_id;
ALTER TABLE profiles DROP COLUMN IF EXISTS tenant_id;
Step 1.3: Add tenant_id to all business tables
Migration File: *_15_add_tenant_id_columns.sql (part 2)
Order: Start with parent tables, then child tables to maintain referential integrity
Parent Tables (no tenant-scoped FKs):
clientsproducts_services
Child Tables (have tenant-scoped FKs):
3. inventory (references products_services)
4. inventory_history (references inventory)
5. employees (references profiles)
6. timesheets (references employees/profiles)
7. invoices (references clients)
8. invoice_line_items (references invoices)
9. quotes (references clients)
10. quote_line_items (references quotes)
11. projects (references clients)
12. tasks (references projects, employees)
13. project_employees (references projects, employees)
14. payments (references invoices)
15. client_communications (references clients)
16. activity_logs (references profiles)
17. api_keys (references profiles)
18. webhooks (references profiles)
19. webhook_logs (references webhooks)
Pattern for each table:
ALTER TABLE <table_name>
ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id);
CREATE INDEX IF NOT EXISTS idx_<table_name>_tenant_id ON <table_name>(tenant_id);
Rollback: Drop indexes and columns in reverse order.
Phase 2: Data Backfill
Step 2.1: Create default tenant
Migration File: *_16_backfill_default_tenant.sql (part 1)
INSERT INTO tenants (name)
VALUES ('Autoch.at')
ON CONFLICT (name) DO NOTHING;
Rollback: DELETE FROM tenants WHERE name = 'Autoch.at'; (only if no data has been backfilled)
Step 2.2: Backfill profiles.tenant_id
Migration File: *_16_backfill_default_tenant.sql (part 2)
UPDATE profiles
SET tenant_id = (SELECT id FROM tenants WHERE name = 'Autoch.at')
WHERE tenant_id IS NULL;
Rollback: UPDATE profiles SET tenant_id = NULL;
Step 2.3: Backfill all business table tenant_id columns
Migration File: *_16_backfill_default_tenant.sql (part 3)
Order: Same as Step 1.3 (parent tables first, then child tables)
Pattern for each table:
UPDATE <table_name>
SET tenant_id = (SELECT id FROM tenants WHERE name = 'Autoch.at')
WHERE tenant_id IS NULL;
Rollback: UPDATE <table_name> SET tenant_id = NULL; (for each table)
Step 2.4: Run consistency checks
Migration File: *_16_backfill_default_tenant.sql (part 4 - validation)
See 06-tenant-consistency-validation.md for validation queries.
Action if mismatches found: Fix data before proceeding to Phase 3.
Phase 3: RLS Rewrite
Step 3.1: Create helper functions
Migration File: *_17_create_tenant_helper_functions.sql
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS uuid
LANGUAGE sql
STABLE
AS $$
SELECT tenant_id FROM profiles WHERE id = auth.uid()
$$;
CREATE OR REPLACE FUNCTION current_user_is_admin()
RETURNS boolean
LANGUAGE sql
STABLE
AS $$
SELECT EXISTS(
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND role = 'admin'
)
$$;
Rollback:
DROP FUNCTION IF EXISTS current_tenant_id();
DROP FUNCTION IF EXISTS current_user_is_admin();
Step 3.2: Replace RLS policies
Migration File: *_18_update_rls_policies_for_tenants.sql
Order:
- Replace read policies first (SELECT)
- Then replace write policies (INSERT, UPDATE, DELETE)
Tables to update (in order):
profiles(critical - affects all other policies)clientsproducts_servicesinventoryinventory_historyemployeestimesheetsinvoicesinvoice_line_itemsquotesquote_line_itemsprojectstasksproject_employeespaymentsclient_communicationsactivity_logsapi_keyswebhookswebhook_logs
Pattern: See 07-rls-policy-migration-strategy.md
Rollback: Restore original policies from migration files (backup before changes)
Phase 4: Application Changes
Note: This phase involves TypeScript/React code changes, not SQL migrations.
Step 4.1: Update TypeScript types
- Add
tenant_id: stringtoProfileinterface - Update
AuthContextto exposetenantId
Step 4.2: Update service layer
- All
INSERToperations must includetenant_id - All
SELECToperations should filter bytenant_id(optional but recommended)
Files to update:
src/types/index.tssrc/context/AuthContext.tsxsrc/services/*.ts(all service files)
Phase 5: Constraints + Hardening
Step 5.1: Make tenant_id NOT NULL
Migration File: *_19_tighten_constraints_and_uniques.sql (part 1)
Order: Same as Step 1.3
Pattern for each table:
ALTER TABLE <table_name>
ALTER COLUMN tenant_id SET NOT NULL;
Rollback:
ALTER TABLE <table_name>
ALTER COLUMN tenant_id DROP NOT NULL;
Step 5.2: Convert global uniques to tenant-scoped uniques
Migration File: *_19_tighten_constraints_and_uniques.sql (part 2)
See 04-unique-constraint-migration.md for details.
Rollback: Drop tenant-scoped uniques and recreate global uniques (only safe before Phase 2 backfill)
Step 5.3: Add composite indexes
Migration File: *_19_tighten_constraints_and_uniques.sql (part 3)
See 03-index-planning.md for composite index specifications.
Rollback: DROP INDEX IF EXISTS <index_name>;
Migration File Sequence
Following existing pattern: YYYYMMDDHHMMSS_##_description.sql
*_14_create_tenants_table.sql*_15_add_tenant_id_columns.sql*_16_backfill_default_tenant.sql*_17_create_tenant_helper_functions.sql*_18_update_rls_policies_for_tenants.sql*_19_tighten_constraints_and_uniques.sql
Testing Strategy
After Phase 1
- Verify all
tenant_idcolumns exist and are nullable - Verify all indexes are created
After Phase 2
- Verify all rows have
tenant_idset - Run consistency validation queries (should return 0 mismatches)
- Verify no NULL
tenant_idvalues remain
After Phase 3
- Test tenant isolation: Create two tenants, verify users can only see their tenant's data
- Test admin permissions within tenant scope
- Test that
USING (true)policies no longer allow cross-tenant access
After Phase 4
- Test all CRUD operations include
tenant_id - Test that reads are tenant-scoped
- Verify application still functions correctly
After Phase 5
- Verify all
tenant_idcolumns are NOT NULL - Verify tenant-scoped uniques work (try inserting duplicate invoice_number in same tenant, should fail)
- Verify tenant-scoped uniques allow duplicates across tenants (try same invoice_number in different tenants, should succeed)
- Test query performance with composite indexes
Rollback Strategy
Full Rollback (Before Phase 2)
- Drop all
tenant_idcolumns and indexes - Drop
tenantstable - Restore original RLS policies
Partial Rollback (After Phase 2, Before Phase 3)
- Keep
tenant_idcolumns but set to NULL - Drop
tenantstable (if no other tenants exist) - Restore original RLS policies
Partial Rollback (After Phase 3, Before Phase 5)
- Revert RLS policies to original
- Keep
tenant_idcolumns nullable - Application must handle NULL
tenant_idgracefully
No Rollback (After Phase 5)
- Once constraints are tightened, rollback becomes very difficult
- Focus on forward fixes rather than rollback
Critical Checkpoints
- Before Phase 2: Backup production database
- After Phase 2: Verify 100% of rows have
tenant_idset - After Phase 3: Test tenant isolation thoroughly in staging
- Before Phase 5: Ensure application is fully updated and tested