Verb

Writing

Postgres row-level security for a multi-tenant AI agent

The short answer

Put row-level security on every table holding tenant data, and set the tenant from a signed session rather than from anything the model or the client can supply. The reason is the asymmetry of the failure modes: with RLS on, a query that forgets its tenant returns nothing, which somebody notices in seconds. With RLS off, a query that forgets its WHERE returns everyone's rows, which nobody notices at all. Choose the failure that is loud.

If an AI agent reads multi-tenant data, the isolation question stops being theoretical. A normal request path is written once and reviewed; an agent composes its own sequence of calls from a sentence somebody typed. You want the boundary somewhere that does not depend on every one of those paths being written carefully.

The argument for RLS is the shape of the failure

Both approaches work when the code is correct. The difference is what happens when it is not, and they are not symmetric:

That asymmetry is the whole argument. You are not choosing between two ways of being right, you are choosing which way you would rather be wrong. Choose the loud one.

Set the tenant from the session, never from an argument

Whatever mechanism you use to tell Postgres who is asking, it must be derived server-side from a signed session. Not a parameter, not a header the client sets, and above all not a tool argument.

A tool that accepts an organisation id is a tool that can be talked into naming a different one, and the attack is not sophisticated: it is a sentence. The safest tool signature for a multi-tenant agent is the one with no tenant parameter at all, because then the question of trusting it does not arise.

Some tables genuinely cannot have it, and that is the dangerous part

There is usually at least one table that has to be read before a tenant context exists: the lookup that resolves which tenant this request even belongs to. It cannot be behind a policy keyed on the answer it is about to provide.

That table needs an explicit predicate on every query instead, and it needs something more important than that: a comment saying why it is exempt. A table sitting outside RLS on purpose and a table that quietly lost its policy look identical in a schema dump. The note is the only thing that tells the next person which one they are looking at, and the next person may be you in six months.

What RLS does not do

It is a floor, not a strategy. It does not stop prompt injection; it bounds what a successful injection can reach, which is a different and still valuable thing. It does not authorise actions within a tenant: an agent acting for a junior user is still inside the same tenant as the admin data, so your application still has to decide what that person may do. And it does not protect against a write your own API was willing to perform.

Pair it with running tools as the signed-in user, so per-user authorisation stays in the system that already knows about it, and treat RLS as the thing that makes a whole class of mistake impossible rather than merely unlikely.

A short checklist

That last one is worth more than it looks. It is the only check that fails when somebody removes a policy for a good reason and forgets to put the predicate back.

Common questions

Does row-level security protect against prompt injection?

Not directly, and it is not meant to. It bounds the blast radius: an injected instruction still cannot reach another tenant's rows because the database refuses them regardless of what the query asks for. Injection is handled separately, by treating tool output as data rather than instructions.

Where does the tenant id come from?

From a signed session, resolved server-side, and never from a parameter the model can fill in. A tool that accepts an organisation id is a tool that can be talked into naming a different one.

Is RLS enough on its own?

No. It is a floor rather than a strategy. Some tables genuinely have to be read before a tenant context exists, and those need an explicit predicate instead, plus a note saying why, because a table that quietly lost its policy looks identical to one that never had it.

Keep reading

Verb is this, built. An AI assistant you embed in your SaaS with one script tag: it calls your own API as the signed-in user, confirms before it changes anything, and logs every action. Free to build and test.