A restriction that lives inside the table, not the query

GRANT controls which tables a role can touch, an all-or-nothing decision per table. A real, common need goes further: a support agent for one region should see that region's tickets, and nothing else, from the same table other agents also query. Row-Level Security enforces that inside the database itself.

ALTER TABLE support_tickets ENABLE ROW LEVEL SECURITY;

CREATE POLICY eu_only ON support_tickets
FOR SELECT TO eu_support_agent USING (region = 'EU');

CREATE POLICY us_only ON support_tickets
FOR SELECT TO us_support_agent USING (region = 'US');

The same query, two roles, genuinely different results

# connected AS eu_support_agent
conn.execute("SELECT ticket_id, region, subject FROM support_tickets")
[(1, 'EU', 'Cannot reset password'), (3, 'EU', 'Feature request')]
# connected AS us_support_agent
conn.execute("SELECT ticket_id, region, subject FROM support_tickets")
[(2, 'US', 'Billing question'), (4, 'US', 'App crash on login')]

Identical query text, SELECT * FROM support_tickets, no WHERE region = ... written anywhere in either query. Each role genuinely only sees its own region's rows, enforced entirely by the database evaluating the matching policy for whichever role is actually connected, not by application code remembering to filter correctly every single time a query gets written.

The gap, confirmed directly rather than assumed

# connected as the table's OWNER
conn.execute("SELECT ticket_id, region FROM support_tickets")
[(1, 'EU'), (2, 'US'), (3, 'EU'), (4, 'US')]

All four rows, both regions, from the exact same table the two restricted roles just queried with genuinely different results. This is real, documented Postgres behavior, not a misconfiguration: a table's owner bypasses Row-Level Security by default, along with any role granted BYPASSRLS. The policies above were never written to apply to the owner at all.

Why this default exists, and why it's easy to trust too much

The reasoning behind the default is real: an owner (typically an admin or ETL role) usually needs full access for legitimate reasons, migrations, backups, cross-region reporting, and forcing every one of those operations through row-level filters would make normal database administration painful. The real risk isn't the default itself, it's assuming RLS alone protects data from every role that can reach the table, including ones that were never meant to see everything but happen to share ownership or elevated privileges with a role that was. A role added for an unrelated reason, but granted table ownership or BYPASSRLS along the way, silently inherits the same bypass, with nothing in the policy definitions themselves warning that it's happening.

Closing the gap when the owner genuinely shouldn't bypass it

ALTER TABLE support_tickets FORCE ROW LEVEL SECURITY;

FORCE ROW LEVEL SECURITY makes the policies apply even to the table's own owner, the explicit, opt-in fix for the cases where "the owner sees everything" isn't actually the intended behavior. Not used by default, and not needed for every table, an admin role legitimately auditing across all regions is a real use case RLS's default owner-bypass is specifically designed to support.

The takeaway

Row-Level Security is a real, database-enforced guarantee for the roles it's actually applied to, confirmed directly above, two agents, one query each, genuinely different real results. It is not automatically a guarantee against every role that can reach the table, table owners bypass it by default, and that's worth confirming directly for any table where "everyone with database access, including admins, should only see their own rows" is actually the intended rule, rather than assumed from the policies alone.