Row-Level Policies: Giving a Principal a Slice of a Table
Table and column allowlists answer one question: which columns can this credential touch? Row-level policies answer the other one: which rows?
The two questions are different, and the second comes up constantly. A per-tenant agent should see one tenant's data, not the whole table. A regional analyst should see EU customers, not US ones. A support credential should see open tickets, not the archive. The table is shared. The slice is not.
How it works
A row-level policy binds a credential to a predicate. PgBeam appends that predicate to every query the credential runs, as an additional WHERE condition, before the query reaches the database.
Conceptually, if the policy on a credential is tenant_id = 'acme', then this query from the agent:
SELECT id, status, total FROM orders WHERE status = 'open';is enforced as if it were:
SELECT id, status, total FROM orders
WHERE status = 'open' AND tenant_id = 'acme';The agent cannot remove the constraint, because the agent never wrote it. It is added at the wire, on top of whatever the agent sent. An agent that tries SELECT * FROM orders with no filter still only gets acme rows. An agent that tries to be clever with a subquery or a join still only sees the slice, because the predicate is applied to the table access itself.
The agent angle
Agents generate SQL you did not write and cannot fully predict. You can spend effort trying to make the agent's prompts produce correctly-scoped queries, or you can stop trusting the agent's SQL to scope itself and enforce the scope underneath it.
A per-tenant support agent is the clean example. You run one agent, configured once, and give each tenant's instance a credential scoped to their tenant_id. The agent's code is identical for every tenant. The isolation is in the credential, not in the prompt. A bug in the agent's query construction cannot leak another tenant's rows, because the rows were filtered before the agent's query ran.
The human angle
The same mechanism scopes people. Give a contractor working on the EU launch a credential constrained to region = 'eu'. Give a support engineer a credential constrained to their assigned accounts. They run normal queries against the normal tables, and each one sees only their slice. You did not build a separate view per person or duplicate the data. One table, many credentials, each pinned to its rows.
This is how you let more principals query production data without widening what any one of them can see. The blast radius of a credential is a slice, not a table.
Why enforce it at the wire
Postgres has row-level security, and it is excellent, when you fully control the database and can manage policies in it. Row-level policies at the proxy give you the same row-scoping for any Postgres host, configured the same way whether the database is RDS, Aurora, self-hosted, or managed, and tied to a PgBeam credential you can issue and revoke in one click rather than a database role you have to provision.
It also composes with everything else. A credential can be read-only, allowlisted to specific columns, masked on the sensitive ones, and row-scoped to its slice, with every layer enforced in the same pass. The slice is just one more thing the policy engine decides before the query reaches your data.
Pair it with read-only by default and masking and you have a credential that can only read, only certain columns, with PII redacted, and only its own rows. That is least privilege you can actually hand out.
Try PgBeam or see the full feature set.