-- Row-Level Security (RLS) Policies -- Run this after migrations to enable tenant isolation at the DB level. -- Idempotent: safe to re-run on every deploy. -- Helper: current tenant context (set via set_config() in Prisma transaction) -- Usage: SELECT set_config('app.current_tenant_id', '', true); -- ─── Tables WITHOUT RLS (cross-tenant by design) ────── ALTER TABLE tenants DISABLE ROW LEVEL SECURITY; ALTER TABLE tenant_hostnames DISABLE ROW LEVEL SECURITY; ALTER TABLE sessions DISABLE ROW LEVEL SECURITY; -- ─── Enable + Force RLS ──────────────────────────────── ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE users FORCE ROW LEVEL SECURITY; ALTER TABLE boards ENABLE ROW LEVEL SECURITY; ALTER TABLE boards FORCE ROW LEVEL SECURITY; ALTER TABLE board_members ENABLE ROW LEVEL SECURITY; ALTER TABLE board_members FORCE ROW LEVEL SECURITY; ALTER TABLE columns ENABLE ROW LEVEL SECURITY; ALTER TABLE columns FORCE ROW LEVEL SECURITY; ALTER TABLE cards ENABLE ROW LEVEL SECURITY; ALTER TABLE cards FORCE ROW LEVEL SECURITY; ALTER TABLE card_assignees ENABLE ROW LEVEL SECURITY; ALTER TABLE card_assignees FORCE ROW LEVEL SECURITY; ALTER TABLE tags ENABLE ROW LEVEL SECURITY; ALTER TABLE tags FORCE ROW LEVEL SECURITY; ALTER TABLE card_labels ENABLE ROW LEVEL SECURITY; ALTER TABLE card_labels FORCE ROW LEVEL SECURITY; ALTER TABLE categories ENABLE ROW LEVEL SECURITY; ALTER TABLE categories FORCE ROW LEVEL SECURITY; ALTER TABLE checklists ENABLE ROW LEVEL SECURITY; ALTER TABLE checklists FORCE ROW LEVEL SECURITY; ALTER TABLE checklist_items ENABLE ROW LEVEL SECURITY; ALTER TABLE checklist_items FORCE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; ALTER TABLE comments FORCE ROW LEVEL SECURITY; ALTER TABLE attachments ENABLE ROW LEVEL SECURITY; ALTER TABLE attachments FORCE ROW LEVEL SECURITY; ALTER TABLE activities ENABLE ROW LEVEL SECURITY; ALTER TABLE activities FORCE ROW LEVEL SECURITY; ALTER TABLE notifications ENABLE ROW LEVEL SECURITY; ALTER TABLE notifications FORCE ROW LEVEL SECURITY; ALTER TABLE board_templates ENABLE ROW LEVEL SECURITY; ALTER TABLE board_templates FORCE ROW LEVEL SECURITY; ALTER TABLE tenant_invites ENABLE ROW LEVEL SECURITY; ALTER TABLE tenant_invites FORCE ROW LEVEL SECURITY; ALTER TABLE webhooks ENABLE ROW LEVEL SECURITY; ALTER TABLE webhooks FORCE ROW LEVEL SECURITY; ALTER TABLE card_watchers ENABLE ROW LEVEL SECURITY; ALTER TABLE card_watchers FORCE ROW LEVEL SECURITY; ALTER TABLE custom_field_defs ENABLE ROW LEVEL SECURITY; ALTER TABLE custom_field_defs FORCE ROW LEVEL SECURITY; ALTER TABLE custom_field_values ENABLE ROW LEVEL SECURITY; ALTER TABLE custom_field_values FORCE ROW LEVEL SECURITY; ALTER TABLE automation_rules ENABLE ROW LEVEL SECURITY; ALTER TABLE automation_rules FORCE ROW LEVEL SECURITY; -- ─── Tenant-scoped tables (direct tenant_id) ────────── -- Users DROP POLICY IF EXISTS users_tenant_isolation ON users; CREATE POLICY users_tenant_isolation ON users USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Boards DROP POLICY IF EXISTS boards_tenant_isolation ON boards; CREATE POLICY boards_tenant_isolation ON boards USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Tags DROP POLICY IF EXISTS tags_tenant_isolation ON tags; CREATE POLICY tags_tenant_isolation ON tags USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Categories DROP POLICY IF EXISTS categories_tenant_isolation ON categories; CREATE POLICY categories_tenant_isolation ON categories USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Board Templates (null tenant_id = system template, visible to all) DROP POLICY IF EXISTS board_templates_tenant_isolation ON board_templates; CREATE POLICY board_templates_tenant_isolation ON board_templates USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id IS NULL OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id IS NULL OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- ─── Board-scoped tables (via join to boards) ───────── -- Board Members DROP POLICY IF EXISTS board_members_tenant_isolation ON board_members; CREATE POLICY board_members_tenant_isolation ON board_members USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Columns DROP POLICY IF EXISTS columns_tenant_isolation ON columns; CREATE POLICY columns_tenant_isolation ON columns USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Activities DROP POLICY IF EXISTS activities_tenant_isolation ON activities; CREATE POLICY activities_tenant_isolation ON activities USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- ─── Card-scoped tables (via join to columns -> boards) ─ -- Cards DROP POLICY IF EXISTS cards_tenant_isolation ON cards; CREATE POLICY cards_tenant_isolation ON cards USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR column_id IN ( SELECT c.id FROM columns c JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR column_id IN ( SELECT c.id FROM columns c JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Card Assignees DROP POLICY IF EXISTS card_assignees_tenant_isolation ON card_assignees; CREATE POLICY card_assignees_tenant_isolation ON card_assignees USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Card Labels DROP POLICY IF EXISTS card_labels_tenant_isolation ON card_labels; CREATE POLICY card_labels_tenant_isolation ON card_labels USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Checklists DROP POLICY IF EXISTS checklists_tenant_isolation ON checklists; CREATE POLICY checklists_tenant_isolation ON checklists USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Checklist Items DROP POLICY IF EXISTS checklist_items_tenant_isolation ON checklist_items; CREATE POLICY checklist_items_tenant_isolation ON checklist_items USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR checklist_id IN ( SELECT ch.id FROM checklists ch JOIN cards ca ON ch.card_id = ca.id JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR checklist_id IN ( SELECT ch.id FROM checklists ch JOIN cards ca ON ch.card_id = ca.id JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Comments DROP POLICY IF EXISTS comments_tenant_isolation ON comments; CREATE POLICY comments_tenant_isolation ON comments USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Attachments DROP POLICY IF EXISTS attachments_tenant_isolation ON attachments; CREATE POLICY attachments_tenant_isolation ON attachments USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- ─── New tenant-scoped tables ──────────────────────── -- Tenant Invites DROP POLICY IF EXISTS tenant_invites_tenant_isolation ON tenant_invites; CREATE POLICY tenant_invites_tenant_isolation ON tenant_invites USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Webhooks DROP POLICY IF EXISTS webhooks_tenant_isolation ON webhooks; CREATE POLICY webhooks_tenant_isolation ON webhooks USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR tenant_id::text = current_setting('app.current_tenant_id', true) ); -- Card Watchers (via card -> column -> board) DROP POLICY IF EXISTS card_watchers_tenant_isolation ON card_watchers; CREATE POLICY card_watchers_tenant_isolation ON card_watchers USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Custom Field Defs (via board) DROP POLICY IF EXISTS custom_field_defs_tenant_isolation ON custom_field_defs; CREATE POLICY custom_field_defs_tenant_isolation ON custom_field_defs USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Custom Field Values (via card -> column -> board) DROP POLICY IF EXISTS custom_field_values_tenant_isolation ON custom_field_values; CREATE POLICY custom_field_values_tenant_isolation ON custom_field_values USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR card_id IN ( SELECT ca.id FROM cards ca JOIN columns c ON ca.column_id = c.id JOIN boards b ON c.board_id = b.id WHERE b.tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- Automation Rules (via board) DROP POLICY IF EXISTS automation_rules_tenant_isolation ON automation_rules; CREATE POLICY automation_rules_tenant_isolation ON automation_rules USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR board_id IN ( SELECT id FROM boards WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ); -- ─── User-scoped tables ─────────────────────────────── -- Notifications DROP POLICY IF EXISTS notifications_tenant_isolation ON notifications; CREATE POLICY notifications_tenant_isolation ON notifications USING ( current_setting('app.current_tenant_id', true) = '__bypass__' OR user_id IN ( SELECT id FROM users WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) ) WITH CHECK ( current_setting('app.current_tenant_id', true) = '__bypass__' OR user_id IN ( SELECT id FROM users WHERE tenant_id::text = current_setting('app.current_tenant_id', true) ) );