Security
Two claims here are worth more than a page of assurances: what the isolation tests do when we deliberately break the code, and what this design explicitly does not protect you from.
Authorization is layered. Row-level security is the last layer, never the only one.
1. verified OIDC identity 2. verified organization membership 3. application authorization 4. resource ownership / grant 5. PostgreSQL row-level security ← defence in depth
Tenant context is derived on the server from a verified token. An organization id supplied by a browser is never trusted, and it is not the thing the database is told.
A missing tenant context is an error, not an empty result
This is the subtle one. PostgreSQL's
current_setting(name, true) returns NULL when a
setting is absent, and a security policy that evaluates to NULL merely
hides the row. So the obvious implementation turns "the server
forgot to establish tenant context" into "this organization has no data" —
a silent wrong answer, in the direction that looks perfectly normal, that
no ordinary test will ever catch.
valid context → uuid context missing → RAISE context malformed → RAISE
The exact boundary, because the first version of this claim was too broad
A policy expression is evaluated per row. So a statement the query planner can prove scans nothing never reaches that function and never raises. We measured where the line actually falls, and it is narrower than we first assumed — an empty table still raises, because the scan is still planned. Only a constant-false qualifier removes it:
SELECT ... FROM cd.worlds WHERE false no raise, returns 0 ← the whole gap SELECT ... FROM cd.audit_events (empty) RAISES SELECT ... FROM cd.worlds LIMIT 0 RAISES SELECT ... FROM cd.worlds RAISES
So the guarantee the database makes is no row may pass without a valid tenant context — not "every statement raises". The difference does not weaken the isolation, since a statement that returns no rows leaks nothing. It does mean the raise is a loud backstop rather than the primary control, and the application establishes tenant context on entering a transaction rather than relying on a policy to notice later.
Row-level security is enabled and forced on every tenant table, so the table owner is policed too. Every exemption from that is a named, listable policy — there are exactly two, both read-only, both belonging to the login path that resolves an identity before a tenant context can exist. Nothing bypasses invisibly.
The negative control
A test suite that passes is not evidence until you know it can fail. The isolation battery has a sabotage mode that swaps the fail-closed function for the naive version described above, and re-runs unchanged.
| Run | Passed | Failed | What it demonstrates |
|---|---|---|---|
| Normal | 196 | 0 | Cross-tenant reads, writes, updates and deletes all refuse. |
| Sabotaged | 186 | 10 | With the naive function installed, an unscoped query returns 0 rows and reports success. The battery catches it. |
| Regression proof | 76 | 120 | With migrations 0008 + 0009 + 0010 + 0011 + 0014 + 0016 + 0017 + 0018 + 0019 + 0020 + 0021 + 0022 + 0023 + 0024 + 0025 + 0026 + 0027 removed, the checks added for those defects fail — so they are load-bearing, not decorative. |
| Concurrency | 34 | 0 | Two real sessions, hand-stepped to a deliberate interleaving — ./worker/test/concurrency.sh. Simultaneous first logins resolve to one principal; simultaneous quota requests cannot oversubscribe. Serial correctness is not concurrent correctness. |
| Worker suite | 486 | 0 | Token verification, tenant context, credential scoping, provider refinement and storage admission — npm test --prefix worker. No account, no network, no dependencies. |
| Worker mutations | 139 | 0 | Each vector breaks one security decision in a copy of the source and re-runs the unmodified suite. A surviving mutation is an untested decision. |
The battery also covers connection re-use, because the production path is transaction-pooled: a context established in one transaction must not survive onto the next request that lands on the same backend.
What this does not solve
Collapsing these two questions is a category error, and calling the first one by the second one's name would be a lie.
| Question | Answered by |
|---|---|
| Who are you, and are you entitled? | Everything on this page. |
| Why can the storage provider not read your data? | Client-side keys. Nothing on this page touches it. |
So: this is authorization, not confidentiality. Nothing here is described as end-to-end encrypted, because nothing here makes it so. Content addressing is actively hostile to access control — if the key is the hash of the content, then knowing the content is knowing the key — and the question of where confidentiality comes from is open and recorded as open rather than quietly assumed away.