Identity and access

Row-level security

Requests do not connect as the service role. Each transaction sets the acting identity, so Postgres policies apply and an API bug cannot become data loss.

Enforcement is deliberately in two places:

  1. In the API, so error responses are Sanity-shaped and the Studio can react to them.
  2. In Postgres, as defence in depth, so a bug in the first cannot become data loss.

For the second to be real, user-initiated work must not run as the service role. Each request opens a transaction and sets the acting identity on it:

sql
begin;
  set local role authenticated;
  set local request.jwt.claims = '{"sub":"<user id>","role":"authenticated"}';
  -- queries run here, with policies reading the current identity
commit;

SET LOCAL is transaction-scoped, which is what makes this safe behind a connection pooler: the setting cannot leak into the next request that borrows the connection.

What the service role is for

Bootstrap, migrations, cron jobs and the orphan sweep. Nothing that serves a user request.

What the policies say

Three predicates, each an allow-list over role names — which is why adding a role that grants nothing (pending) was a safe change rather than a sweeping one: it is refused by all three without any of their bodies changing.

Anonymous connections match only the published-read predicate. This is the mechanism behind a claim made elsewhere on this site: the public frontend does not filter out drafts, it cannot see them.

Reading it yourself

The migrations are plain SQL in the repository. If you want to know exactly what an editor may do, the policy is the answer, and it is fifteen lines.