Build1 distinct publisher3 min readUpdated
A table owner bypasses its own policies with no superuser rights and no BYPASSRLS, and one connection string makes the owner the serving role. Tests seeded with a single tenant never notice.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
The column that decides everything is the last one in the diagnostic. The status query in the dev.to post prints current_user, is_superuser, has_bypassrls, table_owner, rls_enabled and rls_forced, and on the throwaway database it reads app, f, f, app, t, f [4]. Five of those six values are what a reviewer wants to see. The sixth is the one that governs whether the policy on that table runs at all [1].
The arithmetic is short. Two invoices, one per organization, and a count of 2 from a connection that set no tenant at all [5]. That is every row in the table, 100 percent of it, through a policy that was enabled and correctly written [14]. After FORCE ROW LEVEL SECURITY, the same connection running the same query against the same policy returns 0, and returns 1 once the tenant setting is present [6]. Nothing about the predicate changed between those two results [16].
What makes this a deployment problem rather than a documentation footnote is ownership. A Symfony app has one DATABASE_URL, so the role that ran doctrine:migrations:migrate is the role that serves the request, and the role that created the table owns it [7]. Deploy guides arrive there honestly: creating the database owned by the app role is what lets migrations run without a hand-written privilege grant [8]. Postgres does not treat the arrangement as unusual, and the manual says table owners "normally" bypass row security, in a sentence most people read once while looking for CREATE POLICY syntax [1].
The thread that produced this started with a commenter, @to21as, reporting two RLS bugs that were both invisible in tests because the test connection was a superuser [12]. The obvious lesson from that is to stop testing as superuser, and it is not enough. A plain non-superuser role that happens to own the tables behaves the same way, because ownership is a separate bypass from the superuser one [1]. The fixtures conceal it in either case: seeding one organization and asserting that its user sees its own row passes identically whether the policy applies or is inert [10]. The assertion that fails is the negative one, and a suite without it stays green on the day enforcement stops [10].
The failure direction is what carries this through code review. Too many rows, not too few [9]. A filter that returns nothing is found in seconds; a filter that returns everything renders a page that looks correct [9]. What ships is not weakened isolation, it is the state the database was in before any policy was written.
The remedy in the article is one statement per table, added to the migration that already enables RLS [13]. The check is that same six-column query, run in production as the application role: rls_enabled true, rls_forced false, table_owner equal to current_user, and the policy is decoration [4][1].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
PostgreSQL 18 documentation, section 5.9, read on 2026-08-21: superusers and roles with the BYPASSRLS attribute always bypass the row security system when accessing a table; table owners normally bypass row security as well, though a table owner can choose to be subject to row security with ALTER TABLE ... FORCE ROW LEVEL SECURITY.
The measurements were published on dev.to and taken on PostgreSQL 18.3 on a throwaway database, with every command reproduced in the article.
Setup: CREATE ROLE app LOGIN; CREATE DATABASE app_db OWNER app; an invoice table with an organization_id column; ALTER TABLE invoice ENABLE ROW LEVEL SECURITY; and CREATE POLICY tenant_isolation ON invoice USING (organization_id = current_setting('app.organization_id', true)::int).
The diagnostic row for that table read: current_user app, is_superuser f, has_bypassrls f, table_owner app, rls_enabled t, rls_forced f.
With two invoices in the table, one for each of two organizations, the app role connected, set nothing, and ran SELECT count(*) FROM invoice, which returned 2, with no error, no warning and no log line.
After ALTER TABLE invoice FORCE ROW LEVEL SECURITY, the same role on the same connection running the same query got 0; after SET app.organization_id = '2' it got 1.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Reproducible and documentation-anchored, but single-source
The core mechanism is supported twice over: a quoted PostgreSQL 18 documentation passage stating that table owners normally bypass row security, and a published command-by-command reproduction on PostgreSQL 18.3 in which the same role, connection, query and policy returned 2 rows before FORCE ROW LEVEL SECURITY and 0 after. That is verifiable by any reader. Evidence is held below the top band because there is one publisher, one throwaway database and no independent replication, and because the prevalence claim about Symfony deployments rests on the author's reading rather than a survey.
No deployment-scale data
Supplied material contains only three anecdotes about real use: the author's own deploy guide creating an app-owned database, a commenter's two RLS bugs hidden by a superuser test connection, and the author's throwaway-database reproduction. There is no count of affected projects, no telemetry, no repository survey and no incident disclosure, so how widely the owner-as-serving-role pattern or the FORCE remedy is deployed cannot be measured from this cluster.
Mechanism proven; prevalence framing runs slightly ahead
Slightly positive. The demonstrated fact is precise and holds up: with an owner role and rls_forced false, an enabled correct policy filters nothing. The headline and the assertion that this shape holds in almost every Symfony deployment generalise from one reproduction plus the author's reading of deploy guides, with no survey or incident data behind it. The article also self-limits honestly — it flags that FORCE is per-table and decays, and that the split-role fix carries real cost — which keeps the gap small.
Low commercial incentive; some self-referential promotion
The source is a personal practitioner post on dev.to with no vendor, product or sponsorship interest disclosed or apparent: it recommends a built-in PostgreSQL statement and a role-separation pattern, neither of which the author sells. Mild incentive remains in that the piece builds on the author's previous article and his own deploy guide and invites engagement with a commenter, which favours a dramatic framing of a defaults problem.
High on mechanism, moderate on generality
Confidence is high that the technical claim is correct, because it is confirmed by primary documentation and a reproduction whose commands are published, and the remedy is checkable with a single catalogue query. Confidence is capped by the cluster's single publisher, the absence of any independent replication or incident evidence, and the unquantified assertion about how common the owner-as-serving-role deployment shape is.
build
Thirty lines of Doctrine filter, and the query paths where it is simply not there1 distinct publisher
build
RLS Is a Planner Problem: One VOLATILE Function Turns a 3ms Index Scan Into 1.8 Seconds1 distinct publisher
build
Your "Index Only Scan" Did 2,847 Heap Fetches: Covering Indexes Are a Vacuum Problem1 distinct publisher
build
The optional EntityManager is the bug: moving the transaction boundary into AsyncLocalStorage1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 21, 2026