Skip to content

feat!: add Postgres RLS as defense-in-depth on team isolation#79

Merged
FrkAk merged 76 commits into
mainfrom
feat/mymr-151-add-postgres-rls-defense-in-depth
May 17, 2026
Merged

feat!: add Postgres RLS as defense-in-depth on team isolation#79
FrkAk merged 76 commits into
mainfrom
feat/mymr-151-add-postgres-rls-defense-in-depth

Conversation

@FrkAk
Copy link
Copy Markdown
Owner

@FrkAk FrkAk commented May 15, 2026

Summary

Task Reference: [MYMR-151]

BREAKING CHANGE: ALL existings DB needs fresh setup or manual migration to RLS

Adds Postgres Row-Level Security as defense-in-depth on team isolation.

DATABASE_URL now connects as app_user (NOBYPASSRLS). Every read/write runs inside withUserContext(userId), which sets the app.user_id GUC before the query. RLS policies on the 8 public-schema tables (projects, tasks, task_edges, task_assignees, task_acceptance_criteria, task_decisions, task_links, team_invite_code) join neon_auth.member on that GUC to enforce team scope.

Three roles split the trust boundary:

  • app_user — runtime web requests; SELECT on neon_auth.member/organization/user/invitation only. No access to password hashes, session tokens, OAuth tokens, or JWT keys.
  • auth_role — Better Auth runtime; full DML on neon_auth.*, zero grants on public.*.
  • service_role — migrations + two documented bypass call sites (clearOrgMembershipArtifacts, listOrgProjectIdsAsAdmin).

SECURITY DEFINER helpers cover the invite-code join flow and admin lookups so app_user never touches neon_auth.* directly. team_invite_code writes are admin/owner-only; reads are member-only. Cycle detection in edge mutations runs inside the same RLS frame as the insert. withUserContext rejects non-UUID userId at the helper boundary. ESLint forbids bare db.transaction(…) outside lib/db/rls.ts, and a branded Conn parameter on every internal helper turns future bare-call regressions into TypeScript compile errors.

Closes MYMR-151.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation

Testing

  • Tested locally with bun run dev
  • Linting passes (bun run lint)
  • Typecheck passes (bun run typecheck)

Additional:

  • bun run build — passes
  • bun run test:db — 137 pass / 0 fail / 7 skip (RLS-only tests skip under BYPASSRLS lane)
  • bun run test:rls — 263 pass / 0 fail under app_user
  • EXPLAIN confirms InitPlan hoists the GUC subselect

@FrkAk FrkAk requested review from ZeyNor and ulascanzorer as code owners May 15, 2026 10:54
@FrkAk FrkAk self-assigned this May 15, 2026
@FrkAk FrkAk marked this pull request as draft May 15, 2026 12:35
@FrkAk FrkAk force-pushed the feat/mymr-151-add-postgres-rls-defense-in-depth branch from 8a806b9 to 597cc46 Compare May 15, 2026 20:07
@FrkAk FrkAk marked this pull request as ready for review May 15, 2026 21:13
@FrkAk FrkAk force-pushed the feat/mymr-151-add-postgres-rls-defense-in-depth branch from 9623b1d to 59458bf Compare May 17, 2026 00:55
@FrkAk FrkAk marked this pull request as ready for review May 17, 2026 00:57
@FrkAk FrkAk changed the title feat: add Postgres RLS as defense-in-depth on team isolation feat!: add Postgres RLS as defense-in-depth on team isolation May 17, 2026
@FrkAk FrkAk merged commit 02d0234 into main May 17, 2026
5 checks passed
@FrkAk FrkAk deleted the feat/mymr-151-add-postgres-rls-defense-in-depth branch May 17, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants