1.3.2 Project RLS policies + active-project resolution in workspace context
Estimate: 16m · Depends on: 1.3.1
The structural multi-tenant gate for projects, plus the read path that resolves a member's active project. (1) Postgres RLS on the project table, keyed off the existing app.workspace_id session GUC that 1.2.3 established — a project row is visible/writable only when project.workspace_id = current_setting('app.workspace_id', true). Queries without the GUC see no rows (read AND write). This reuses the withWorkspaceContext machinery from 1.2.3 unchanged; projects just gain a policy. (2) Active-project resolution: a server helper getActiveProject() (analogue of getWorkspaceContext()) that reads the member's activeProjectId (or falls back to the workspace's first project, or null if the workspace has none) inside a workspace-scoped transaction.
Why RLS here too, not just app-layer filtering: defense-in-depth, identical to the rationale in 1.2.3. The service always filters by workspaceId, but RLS catches any future endpoint that forgets to. The Story-level "structurally impossible" claim requires the DB-layer gate, validated by 1.3.6's direct-DB test.
What you'll do: Add a migration (add_project_rls) enabling RLS + the workspace-match policy on project, granting the prodect_app role (from 1.2.3) the usual CRUD. Add projectsService.getActiveProject(userId, workspaceId) returning a DTO or null, reading via withWorkspaceContext. Wire it into lib/workspaces/index.ts or a sibling lib/projects/index.ts export so server components can read it the same way they read the workspace context. Note finding #5 (dev/CI connects as a BYPASSRLS superuser) — the RLS test in 1.3.6 must SET LOCAL ROLE prodect_app to make the policy bite; do NOT copy the direct-Prisma pattern from lib/workspaces/middleware.ts (finding #5/#7).
Acceptance criteria
- Migration
add_project_rlsenables RLS onprojectand creates a policy matchingworkspace_idagainstcurrent_setting('app.workspace_id', true); grantsprodect_appCRUD. projectsService.getActiveProject(userId, workspaceId)resolves the member'sactiveProjectId, falling back to the workspace's first project (createdAt asc) or null; runs insidewithWorkspaceContext; returns a DTO.- A server-side
getActiveProject()helper reads session + active workspace + active project, mirroringgetWorkspaceContext()'s shape. - Existing 11 RLS tests stay green; no behavior change to workspace RLS. 4-layer rule respected. All quality gates green.
Context refs
prisma/migrations/…add_workspace_rls— the exact RLS migration pattern to mirrorlib/workspaces/context.ts(withWorkspaceContext) +lib/workspaces/index.ts(getWorkspaceContext) — the resolver shape to analogizetests/workspace-rls.test.ts— theSET LOCAL ROLE prodect_apptest harnessPRODECT_FINDINGS.md#5/#7 — RLS-inert-under-superuser + the middleware direct-Prisma anti-pattern to NOT copy