Tenant isolation
Isolation is enforced by the database rather than by the application, and it is asserted rather than described. This page is what the assertions actually ask.
185assertions, and never fewer
Each one assumes a real signed-in identity, sets the claim the database reads, and asks a question through the same interface the application uses. They run against the live schema inside a transaction that is rolled back, and the deploy gate will not pass with one of them failing.
Why it is in the database and not in the code
A query that forgets its where account_id = clause is one typo, in one screen, in one release — and it returns another customer’s rows without erroring. Row-level security moves the check underneath every query, so a screen that forgets returns nothing instead of returning somebody else’s copy.
The interface the tests use is the one the application uses: they assume the authenticated role and set the claim the database reads for identity, rather than connecting as an administrator and asking politely.
What is asked, and what is answered
- Can one customer’s people read another’s work?
- No. Posts, brands, campaigns, uploaded images, the account row itself, the roster, the pending invites and the grants on another account’s brands. Each one comes back empty.
- Can they write into it?
- No. An insert is refused. An update and a delete affect no rows, which is a different answer from a refusal and is asserted separately.
- Can somebody carry a row across instead of reaching it?
- No. An editor seated in two accounts — the ordinary position of a consultant with two agency clients — cannot move a brand, a campaign or a post from one account into the other. Nor can the account owner. Nor can the table owner, where row-level security does not apply at all, because that guard is a trigger rather than a policy.
- Can a brand-scoped member see the other brands?
- No. They read their own brand’s posts, images, generation audits, release records and review links, and none of another’s. Their storage key opens their brand’s path in the bucket and no other. They can mint a review link on their own brand’s post and are refused on anyone else’s.
- Can they reach the account level from inside a brand?
- No roster, no account-wide overview, no access map, no invitations, and the account rules are refused. They can set and tighten the rules on the brand they were given, and are refused on every other brand in the same account.
- Can the audited party edit the audit?
- No. Generation audits, release records and the ledger of accepted findings each carry a policy for reading and a policy for inserting, and none for updating or deleting — so an update or a delete affects no rows, for anyone, including the account owner who released.
- What does somebody with no membership see?
- Nothing. Six tables asked directly — accounts, brands, campaigns, posts, images, templates — and every one of them returns nothing.
- And the links handed to people with no login at all?
- A token resolves to exactly one post and is not a door into the brand or the campaign around it. Expired and revoked both fail closed, an unknown token is indistinguishable from a revoked one, and an anonymous caller cannot execute the functions behind any of it.
What this does not claim
These assertions cover the database. They say nothing about a member of your own team who holds a seat legitimately and behaves badly with it — that is what the register is for — and nothing about the security of your own devices.
One role does bypass row-level security by design: the service key held by the server, used by exactly one function, which re-checks authorisation itself at every step. It is never in the browser bundle.