-- Migration 026 — per-project access grants (RBAC v2). -- -- v1 auth is flat: any logged-in user can do everything. This adds per-project -- scoping. A grant targets either a user or a group (polymorphic subject) and -- carries a level: 'view' (read-only) or 'edit' (read-write). Admins bypass all -- of this in code (authz.js) and need no rows here. -- -- subject_id is intentionally NOT a foreign key — it points at either users.id -- or groups.id depending on subject_type. Rows are cleaned up when the project -- is deleted (FK cascade). A deleted user/group leaves an orphan row that -- resolves to nobody (harmless); a later sweep can prune them if desired. DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'access_level') THEN CREATE TYPE access_level AS ENUM ('view', 'edit'); END IF; END $$; CREATE TABLE IF NOT EXISTS project_access ( project_id UUID NOT NULL REFERENCES projects ON DELETE CASCADE, subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')), subject_id UUID NOT NULL, level access_level NOT NULL DEFAULT 'view', granted_by UUID REFERENCES users ON DELETE SET NULL, granted_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (project_id, subject_type, subject_id) ); CREATE INDEX IF NOT EXISTS idx_project_access_subject ON project_access (subject_type, subject_id);