RLS makes data isolation a database guarantee, not just an application convention. If a handler forgets a WHERE owner_id = …, Postgres still refuses to return another owner's rows. For a multi-tenant product handling personal data, that backstop is worth the complexity.
- The Worker resolves a principal (owner id) per request.
withPrincipal(db, ownerId, fn)opens a transaction and runsselect set_config('app.current_owner', <ownerId>, true)- a transaction-scoped GUC.- Every table has policies for the
oche_approle:sessions:owner_id = current_setting('app.current_owner')::uuidplayers/score_events: ownership via a subquery to the parent session.
- Tables are
FORCE ROW LEVEL SECURITY, so even the table owner is subject to policies. oche_appisNOBYPASSRLSand granted only CRUD - never DDL or bypass.
score_events has SELECT + INSERT policies but no UPDATE/DELETE → an append-only audit trail.
npm run db:rls:checkfails if any table isn't enabled+forced, or ifoche_appcan bypass.npm run test:rlsproves owner A cannot read/update owner B's rows.
Replace the GUC stand-in with real auth. With Neon RLS, a verified JWT exposes auth.user_id(); policies become, e.g.:
import { crudPolicy, authenticatedRole, authUid } from 'drizzle-orm/neon';
// on sessions:
crudPolicy({ role: authenticatedRole, read: authUid(t.ownerId), modify: authUid(t.ownerId) });The Worker forwards the user's JWT to Postgres; no app-set GUC needed. Everything else (FORCE, append-only events, least-privilege role) stays.