-- 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; -- ─── 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) ) ); -- ─── 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) ) );