RowShield
Guides

The new column your policy never mentioned

ALTER TABLE ADD COLUMN feels inert, which is why it ships casually. But policies predicate over columns, and columns carry data. Add the wrong one and either exposure changes without a single policy edit, or a future migration’s assumptions crack against predicates written months ago. PostgREST sharpens the point: clients name the columns they want through the select parameter, so the moment a column commits, every caller who could already read the table can request it by name.

As everywhere in this cluster: RowShield sees these effects through catalog snapshots diffed scan-over-scan — product behaviour, not a named rule — and this page gives you the mechanism plus self-serve checks.

RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.

The two ways additions bite

First, direct exposure. A sensitive column added to a broadly readable table starts flowing to every existing caller immediately — no policy edit required, because the policy already grants the table. Generated scaffolding loves adding denormalized fields like owner_email beside owner_id, and broad listing endpoints begin serving email addresses the same deploy. The policy never mentioned columns because row-level security governs rows, not projections; column visibility rides entirely on table readability, so the addition inherits whatever audience the parent already has.

Second, assumption decay. Predicates comparing typed expectations — enum checks, JSON key probes, tenant scoping through lookup joins — degrade when migrations alter the shapes they assume. Nothing errors; conditions simply start matching differently, and authorization quietly becomes probabilistic.

Checking a fresh addition yourself

After any ADD COLUMN on a governed table, interrogate the catalog from two directions — who can reach the table, and what else projects it:

-- Who can read the table this column joined?
SELECT policyname, roles, cmd, qual
FROM pg_catalog.pg_policies
WHERE schemaname='public' AND tablename='invoices';

-- And is anything else exposing it (views over the table)?
SELECT viewname FROM pg_catalog.pg_views
WHERE schemaname='public'
  AND definition ILIKE '%invoices%';

Designing additions defensively

Three habits prevent the class. Ship sensitive columns NULL-defaulted and backfill under review, so their first moments are not their widest. Prefer separate governance tables for genuinely sensitive attributes — joining at read time keeps listing endpoints narrow by construction. And document intent beside DDL: a comment naming who may see the new field converts the next reviewer’s guess into a check. Defaults shift the calculus once more: modern Postgres applies non-volatile defaults without rewriting the table, so a sensitive default populates every historical row instantly at read time. Decide who may see the populated past, not merely the populated future, before the migration runs.

Variations you will meet

Plain nullable additions are only the common case, and the variations carry their own twists. Columns added with defaults populate history instantly at read time, so a sensitive value materialises across every existing row once the migration commits. Generated columns compute themselves from other columns under table ownership, surfacing combinations of fields that were only ever meant to be judged separately.

Drop-and-re-add patterns deserve suspicion too: recreating a column under a new name resets it to NULL everywhere, while predicates still referencing the old spelling either error or silently compare against nothing. Additions landing on tables behind views widen an audience nobody revisited, because views re-project automatically.

Every variation converges operationally: after DDL touching a governed table, re-read the governing policies, re-probe, and confirm the response shape matches intention before calling the deploy finished.

What the alert looks like

Under monitoring, the addition surfaces as structural drift in context: the column appears in the same scan window as any policy movement around it, turning "something changed" into "this changed, here". If probe results shift — a previously filtered response pattern changing shape — the finding rides alongside with severities intact.

That pairing matters more than either half alone. Column-plus-policy co-movement is the signature of both accidents and fixes; seeing them together is how you tell remediation from regression without opening a single editor.

Frequently asked

Can adding a column really cause a leak?
Yes — indirectly but reliably. Row-level security decides which rows a caller reaches, never which columns travel back, so any read path over the table begins serving the new data the moment it exists. Sensitive-by-intent columns land on widely readable tables surprisingly often, because generated scaffolding denormalises for convenience. Check who can already read the parent table first.
Does RowShield flag specific columns?
No named rule targets individual columns. Exposure findings report at table granularity: readable tables, plus observed columns inferred through probe responses. What catches an addition specifically is drift diffing — scheduled snapshots show the column appearing between two scans beside any policy movement in the same window, product behaviour rather than rule-backed automation.
What about GENERATED columns?
Same story with a twist: generation expressions run under table ownership, so a generated field can materialise combinations its source policies never intended to expose together. Stored generated columns flow to callers exactly like ordinary ones, and because the database maintains them automatically, nothing in application code reminds anyone the derived value now exists.
Is there a safe default for sensitive fields?
Yes — separate table, owner-scoped policy, join on read. The pattern costs one extra query at write time and buys certainty at read time, which is usually the trade you wanted anyway. Where a separate table genuinely does not fit, ship the column NULL-defaulted, schedule its backfill under review, and re-probe the endpoint before reopening traffic.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
add column broke rls policy supabasenew column exposed supabaseschema change affected policysupabase alter table add column security