Row-level security
Database rules that filter rows per user automatically, so an ordinary app query returns only the rows that user is allowed to see.
See it
What it is
Row-level security (RLS) moves the 'can this person see this row?' check out of your application code and into the database itself. You enable it on a table and attach policies: a USING expression that filters what a role can read, and a WITH CHECK expression that constrains what it can write. Postgres then silently appends your policy to every query, so a forgotten WHERE clause in some endpoint cannot leak another tenant's rows.
It earns its keep in two situations: multi-tenant products where one table holds many customers' data, and architectures where clients talk to the database more or less directly (the Supabase model, where the browser holds a JWT and RLS is the entire authorization layer). If every query already goes through one trusted server-side data layer, RLS is defense in depth rather than the main event.
Gotchas that bite everyone: creating a policy does nothing until you also run ALTER TABLE ... ENABLE ROW LEVEL SECURITY, so people ship wide-open tables believing they are locked down. Then the bypass list: the table owner skips policies until you also run FORCE ROW LEVEL SECURITY, and superusers plus any role carrying the BYPASSRLS attribute skip them regardless, so the role your app connects as must be neither the owner nor BYPASSRLS or the whole thing is decoration. A hosted provider's 'service role' key is just a preconfigured version of that bypass, so anything running with one does its own filtering. Policies also run per candidate row: if the column your policy compares is not indexed, query plans fall off a cliff at scale.
Ask AI for it
Lock this Postgres table down with row-level security for a multi-tenant app. First create a dedicated application role that does not own the table and does not have BYPASSRLS or superuser, and have the app connect as that role. Run ALTER TABLE <table> ENABLE ROW LEVEL SECURITY and ALTER TABLE <table> FORCE ROW LEVEL SECURITY, then create separate policies for SELECT, INSERT, UPDATE, and DELETE that match tenant_id against the current request's tenant claim. Set that claim transaction-locally, never per connection, with set_config('app.tenant_id', $1, true) or SET LOCAL at the start of every transaction, so a pooled connection handed to the next request cannot inherit the previous tenant's context. Use USING for reads and WITH CHECK on writes so rows cannot be inserted into another tenant. Add an index on tenant_id, and give me two tests: one proving tenant A gets zero rows belonging to tenant B, and one proving a reused pooled connection with no tenant set returns nothing rather than everything.