RLS Policy Migration Strategy
Overview
Current RLS policies using USING (true) allow any authenticated user to read all rows, which is incompatible with multitenancy. All policies must be replaced with tenant-scoped policies that restrict access to the user's tenant only.
Helper Functions
Before updating policies, create these helper functions (Phase 3, Step 3.1):
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'
)
$$;
Policy Replacement Pattern
General Pattern for Tenant-Scoped Policies
Read (SELECT) Policies:
- Replace
USING (true)withUSING (tenant_id = current_tenant_id()) - All users in a tenant can read their tenant's data
Write Policies (INSERT/UPDATE/DELETE):
- Add tenant check:
tenant_id = current_tenant_id() - Add role check if needed:
current_user_is_admin() - Use
WITH CHECKclause for INSERT/UPDATE to enforce tenant_id matches
Tables Requiring Policy Updates
1. profiles
Current Policies (from migration 01_create_auth_users_table.sql):
- "Users can read own profile" - Keep (already tenant-safe via
auth.uid() = id) - "Admins can read all profiles" - REPLACE (currently global)
- "Users can update own profile" - Keep (already tenant-safe)
- "Admins can update all profiles" - REPLACE (currently global)
New Policies:
-- Drop global admin policies
DROP POLICY IF EXISTS "Admins can read all profiles" ON profiles;
DROP POLICY IF EXISTS "Admins can update all profiles" ON profiles;
-- Tenant-scoped admin policies
CREATE POLICY "Tenant admins can read tenant profiles"
ON profiles FOR SELECT
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update tenant profiles"
ON profiles FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
Note: Keep "Users can read own profile" and "Users can update own profile" as-is.
2. clients
Current Policies (from migration 02_create_clients_table.sql):
- "Authenticated users can read clients" - REPLACE (
USING (true)) - "Admins can create clients" - UPDATE (add tenant check)
- "Admins can update clients" - UPDATE (add tenant check)
- "Admins can delete clients" - UPDATE (add tenant check)
New Policies:
-- Drop old policies
DROP POLICY IF EXISTS "Authenticated users can read clients" ON clients;
DROP POLICY IF EXISTS "Admins can create clients" ON clients;
DROP POLICY IF EXISTS "Admins can update clients" ON clients;
DROP POLICY IF EXISTS "Admins can delete clients" ON clients;
-- Tenant-scoped read
CREATE POLICY "Tenant users can read clients"
ON clients FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
-- Tenant-scoped create
CREATE POLICY "Tenant admins can create clients"
ON clients FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
-- Tenant-scoped update
CREATE POLICY "Tenant admins can update clients"
ON clients FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
-- Tenant-scoped delete
CREATE POLICY "Tenant admins can delete clients"
ON clients FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
3. products_services
Current Policies (from migration 03_create_products_services_table.sql):
- "Authenticated users can read products_services" - REPLACE (
USING (true)) - "Admins can create products_services" - UPDATE (add tenant check)
- "Admins can update products_services" - UPDATE (add tenant check)
- "Admins can delete products_services" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read products_services" ON products_services;
DROP POLICY IF EXISTS "Admins can create products_services" ON products_services;
DROP POLICY IF EXISTS "Admins can update products_services" ON products_services;
DROP POLICY IF EXISTS "Admins can delete products_services" ON products_services;
CREATE POLICY "Tenant users can read products_services"
ON products_services FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create products_services"
ON products_services FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update products_services"
ON products_services FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete products_services"
ON products_services FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
4. inventory
Current Policies (from migration 04_create_inventory_table.sql):
- "Authenticated users can read inventory" - REPLACE (
USING (true)) - "Admins can update inventory" - UPDATE (add tenant check)
- "Authenticated users can read inventory_history" - REPLACE (
USING (true)) - "Admins can create inventory_history" - UPDATE (add tenant check)
New Policies:
-- inventory table
DROP POLICY IF EXISTS "Authenticated users can read inventory" ON inventory;
DROP POLICY IF EXISTS "Admins can update inventory" ON inventory;
CREATE POLICY "Tenant users can read inventory"
ON inventory FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can update inventory"
ON inventory FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
-- inventory_history table
DROP POLICY IF EXISTS "Authenticated users can read inventory_history" ON inventory_history;
DROP POLICY IF EXISTS "Admins can create inventory_history" ON inventory_history;
CREATE POLICY "Tenant users can read inventory_history"
ON inventory_history FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create inventory_history"
ON inventory_history FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
5. employees
Current Policies (from migration 05_create_employees_table.sql):
- "Users can read own employee data" - Keep (already tenant-safe via
auth.uid() = id) - "Admins can read all employees" - REPLACE (currently global)
- "Users can update own employee data" - Keep (already tenant-safe)
- "Admins can update all employees" - REPLACE (currently global)
- "Admins can insert employees" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Admins can read all employees" ON employees;
DROP POLICY IF EXISTS "Admins can update all employees" ON employees;
DROP POLICY IF EXISTS "Admins can insert employees" ON employees;
CREATE POLICY "Tenant admins can read tenant employees"
ON employees FOR SELECT
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update tenant employees"
ON employees FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can insert employees"
ON employees FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
6. timesheets
Current Policies (from migration 06_create_timesheets_table.sql):
- "Employees can read own timesheets" - Keep (already tenant-safe via
auth.uid() = employee_id) - "Admins can read all timesheets" - REPLACE (currently global)
- "Employees can create own timesheets" - Keep (already tenant-safe)
- "Employees can update own timesheets" - Keep (already tenant-safe)
- "Admins can update timesheets" - REPLACE (currently global)
New Policies:
DROP POLICY IF EXISTS "Admins can read all timesheets" ON timesheets;
DROP POLICY IF EXISTS "Admins can update timesheets" ON timesheets;
CREATE POLICY "Tenant admins can read tenant timesheets"
ON timesheets FOR SELECT
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update tenant timesheets"
ON timesheets FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
7. invoices
Current Policies (from migration 07_create_invoices_table.sql):
- "Authenticated users can read invoices" - REPLACE (
USING (true)) - "Admins can create invoices" - UPDATE (add tenant check)
- "Admins can update invoices" - UPDATE (add tenant check)
- "Admins can delete invoices" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read invoices" ON invoices;
DROP POLICY IF EXISTS "Admins can create invoices" ON invoices;
DROP POLICY IF EXISTS "Admins can update invoices" ON invoices;
DROP POLICY IF EXISTS "Admins can delete invoices" ON invoices;
CREATE POLICY "Tenant users can read invoices"
ON invoices FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create invoices"
ON invoices FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update invoices"
ON invoices FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete invoices"
ON invoices FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
8. invoice_line_items
Current Policies (from migration 07_create_invoices_table.sql):
- "Authenticated users can read invoice_line_items" - REPLACE (currently checks invoice existence, not tenant)
- "Admins can insert invoice_line_items" - UPDATE (add tenant check)
- "Admins can delete invoice_line_items" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read invoice_line_items" ON invoice_line_items;
DROP POLICY IF EXISTS "Admins can insert invoice_line_items" ON invoice_line_items;
DROP POLICY IF EXISTS "Admins can delete invoice_line_items" ON invoice_line_items;
CREATE POLICY "Tenant users can read invoice_line_items"
ON invoice_line_items FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can insert invoice_line_items"
ON invoice_line_items FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete invoice_line_items"
ON invoice_line_items FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
9. quotes
Current Policies (from migration 11_create_quotes_table.sql):
- "Authenticated users can read quotes" - REPLACE (
USING (true)) - "Admins can create quotes" - UPDATE (add tenant check)
- "Admins can update quotes" - UPDATE (add tenant check)
- "Admins can delete quotes" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read quotes" ON quotes;
DROP POLICY IF EXISTS "Admins can create quotes" ON quotes;
DROP POLICY IF EXISTS "Admins can update quotes" ON quotes;
DROP POLICY IF EXISTS "Admins can delete quotes" ON quotes;
CREATE POLICY "Tenant users can read quotes"
ON quotes FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create quotes"
ON quotes FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update quotes"
ON quotes FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete quotes"
ON quotes FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
10. quote_line_items
Current Policies (from migration 11_create_quotes_table.sql):
- "Authenticated users can read quote_line_items" - REPLACE (currently checks quote existence, not tenant)
- "Admins can insert quote_line_items" - UPDATE (add tenant check)
- "Admins can update quote_line_items" - UPDATE (add tenant check)
- "Admins can delete quote_line_items" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read quote_line_items" ON quote_line_items;
DROP POLICY IF EXISTS "Admins can insert quote_line_items" ON quote_line_items;
DROP POLICY IF EXISTS "Admins can update quote_line_items" ON quote_line_items;
DROP POLICY IF EXISTS "Admins can delete quote_line_items" ON quote_line_items;
CREATE POLICY "Tenant users can read quote_line_items"
ON quote_line_items FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can insert quote_line_items"
ON quote_line_items FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update quote_line_items"
ON quote_line_items FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete quote_line_items"
ON quote_line_items FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
11. projects
Current Policies (from migration 12_create_projects_table.sql):
- "Authenticated users can read projects" - REPLACE (
USING (true)) - "Admins can create projects" - UPDATE (add tenant check)
- "Admins can update projects" - UPDATE (add tenant check)
- "Admins can delete projects" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read projects" ON projects;
DROP POLICY IF EXISTS "Admins can create projects" ON projects;
DROP POLICY IF EXISTS "Admins can update projects" ON projects;
DROP POLICY IF EXISTS "Admins can delete projects" ON projects;
CREATE POLICY "Tenant users can read projects"
ON projects FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create projects"
ON projects FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update projects"
ON projects FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete projects"
ON projects FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
12. tasks
Current Policies (from migration 12_create_projects_table.sql):
- "Authenticated users can read tasks" - REPLACE (
USING (true)) - "Admins can create tasks" - UPDATE (add tenant check)
- "Admins and assigned employees can update tasks" - UPDATE (add tenant check)
- "Admins can delete tasks" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read tasks" ON tasks;
DROP POLICY IF EXISTS "Admins can create tasks" ON tasks;
DROP POLICY IF EXISTS "Admins and assigned employees can update tasks" ON tasks;
DROP POLICY IF EXISTS "Admins can delete tasks" ON tasks;
CREATE POLICY "Tenant users can read tasks"
ON tasks FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create tasks"
ON tasks FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins and assigned employees can update tasks"
ON tasks FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND (
current_user_is_admin()
OR assigned_to = auth.uid()
)
)
WITH CHECK (
tenant_id = current_tenant_id()
AND (
current_user_is_admin()
OR assigned_to = auth.uid()
)
);
CREATE POLICY "Tenant admins can delete tasks"
ON tasks FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
13. project_employees
Current Policies (from migration 12_create_projects_table.sql):
- "Authenticated users can read project_employees" - REPLACE (
USING (true)) - "Admins can manage project_employees" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read project_employees" ON project_employees;
DROP POLICY IF EXISTS "Admins can manage project_employees" ON project_employees;
CREATE POLICY "Tenant users can read project_employees"
ON project_employees FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can manage project_employees"
ON project_employees FOR ALL
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
14. payments
Current Policies (from migration 10_create_payments_table.sql):
- "Authenticated users can view payments" - REPLACE (
USING (true)) - "Admins can create payments" - UPDATE (add tenant check)
- "Admins can update payments" - UPDATE (add tenant check)
- "Admins can delete payments" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can view payments" ON payments;
DROP POLICY IF EXISTS "Admins can create payments" ON payments;
DROP POLICY IF EXISTS "Admins can update payments" ON payments;
DROP POLICY IF EXISTS "Admins can delete payments" ON payments;
CREATE POLICY "Tenant users can read payments"
ON payments FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant admins can create payments"
ON payments FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can update payments"
ON payments FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
)
WITH CHECK (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
CREATE POLICY "Tenant admins can delete payments"
ON payments FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
15. client_communications
Current Policies (from migration 13_create_client_communications_table.sql):
- "Authenticated users can read client_communications" - REPLACE (
USING (true)) - "Admins and employees can create client_communications" - UPDATE (add tenant check)
- "Creator or admin can update client_communications" - UPDATE (add tenant check)
- "Creator or admin can delete client_communications" - UPDATE (add tenant check)
New Policies:
DROP POLICY IF EXISTS "Authenticated users can read client_communications" ON client_communications;
DROP POLICY IF EXISTS "Admins and employees can create client_communications" ON client_communications;
DROP POLICY IF EXISTS "Creator or admin can update client_communications" ON client_communications;
DROP POLICY IF EXISTS "Creator or admin can delete client_communications" ON client_communications;
CREATE POLICY "Tenant users can read client_communications"
ON client_communications FOR SELECT
TO authenticated
USING (tenant_id = current_tenant_id());
CREATE POLICY "Tenant users can create client_communications"
ON client_communications FOR INSERT
TO authenticated
WITH CHECK (
tenant_id = current_tenant_id()
AND EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND role IN ('admin', 'employee')
)
);
CREATE POLICY "Tenant creator or admin can update client_communications"
ON client_communications FOR UPDATE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND (
created_by = auth.uid()
OR current_user_is_admin()
)
)
WITH CHECK (
tenant_id = current_tenant_id()
AND (
created_by = auth.uid()
OR current_user_is_admin()
)
);
CREATE POLICY "Tenant creator or admin can delete client_communications"
ON client_communications FOR DELETE
TO authenticated
USING (
tenant_id = current_tenant_id()
AND (
created_by = auth.uid()
OR current_user_is_admin()
)
);
16. activity_logs
Current Policies (from migration 08_create_activity_log_table.sql):
- "Admins can read all activity logs" - REPLACE (currently global)
- "Users can read own activity logs" - Keep (already tenant-safe via
auth.uid() = user_id) - "System can create activity logs" - Keep (already tenant-safe)
New Policies:
DROP POLICY IF EXISTS "Admins can read all activity logs" ON activity_logs;
CREATE POLICY "Tenant admins can read tenant activity logs"
ON activity_logs FOR SELECT
TO authenticated
USING (
tenant_id = current_tenant_id()
AND current_user_is_admin()
);
17. api_keys
Current Policies (from migration 09_create_api_keys_table.sql):
- "Users can read own API keys" - Keep (already tenant-safe via
auth.uid() = user_id) - "Users can create own API keys" - UPDATE (add tenant check)
- "Users can delete own API keys" - Keep (already tenant-safe)
New Policies:
DROP POLICY IF EXISTS "Users can create own API keys" ON api_keys;
CREATE POLICY "Tenant users can create own API keys"
ON api_keys FOR INSERT
TO authenticated
WITH CHECK (
user_id = auth.uid()
AND tenant_id = current_tenant_id()
);
18. webhooks
Current Policies (from migration 09_create_api_keys_table.sql):
- "Users can read own webhooks" - Keep (already tenant-safe)
- "Users can create own webhooks" - UPDATE (add tenant check)
- "Users can update own webhooks" - Keep (already tenant-safe)
- "Users can delete own webhooks" - Keep (already tenant-safe)
New Policies:
DROP POLICY IF EXISTS "Users can create own webhooks" ON webhooks;
CREATE POLICY "Tenant users can create own webhooks"
ON webhooks FOR INSERT
TO authenticated
WITH CHECK (
user_id = auth.uid()
AND tenant_id = current_tenant_id()
);
19. webhook_logs
Current Policies (from migration 09_create_api_keys_table.sql):
- "Users can read own webhook logs" - Keep (already tenant-safe via webhook ownership check)
No changes needed - policy already checks webhook ownership, which is tenant-scoped.
Execution Order
- Create helper functions first (Phase 3, Step 3.1)
- Update
profilespolicies first (affects all other policies) - Update parent tables (clients, products_services) before child tables
- Update all other tables in dependency order
- Test tenant isolation after each major group
Testing After Policy Updates
- Create two test tenants
- Create test data in each tenant
- Verify users in Tenant A cannot see Tenant B's data
- Verify admins in Tenant A cannot modify Tenant B's data
- Verify all CRUD operations work within tenant scope