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).
add_project_rls enables RLS on project and creates a policy matching workspace_id against current_setting('app.workspace_id', true); grants prodect_app CRUD.projectsService.getActiveProject(userId, workspaceId) resolves the member's activeProjectId, falling back to the workspace's first project (createdAt asc) or null; runs inside withWorkspaceContext; returns a DTO.getActiveProject() helper reads session + active workspace + active project, mirroring getWorkspaceContext()'s shape.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 — the SET LOCAL ROLE prodect_app test harnessPRODECT_FINDINGS.md #5/#7 — RLS-inert-under-superuser + the middleware direct-Prisma anti-pattern to NOT copy