Why move the rules down
An access rule enforced in application code is enforced once per code path. An access rule enforced in the database is enforced once, full stop — including for the migration script, the admin tool, and the endpoint a developer adds next quarter without reading the handbook.
That is the whole argument for row level security, and it holds. What follows is the part the argument leaves out.
One: a missing policy is silence, not an error
A query against a table with RLS enabled and no matching policy does not fail. It returns zero rows. Your page renders an empty state, your logs stay clean, and you spend an hour looking at the wrong layer.
Our rule now: every table gets its policies in the same migration that enables RLS, and every empty list in the UI has to be distinguishable from an error.
Two: policies are per operation
SELECT, INSERT, UPDATE and DELETE each need their own policy. A public site that reads fine and silently drops form submissions is almost always a table with a read policy and no insert policy.
Three: the anon key is a public key
It ships to the browser. It is not a secret and was never meant to be one. Everything protecting your data is the policy set — which means the policies deserve the same review attention as authentication code.
- Gate public reads on an explicit flag:
is_active,status = 'published'. - Never write a policy that reads
using (true)on a table with private columns. - Keep write access to authenticated roles, and test it as an anonymous user.
Four: test the negative case
It is easy to confirm the right user can see their row. The test that matters is the other one — that a second user cannot. Write it first, watch it fail before the policy exists, and keep it.
Part of the team building and running the products behind these posts at Hedaya Global Solutions.