RowShield
Guides

Access-control posture for GDPR-minded teams on Supabase

Teams handling personal data on Supabase eventually ask a practical question: what does a defensible access-control posture actually look like at the database layer? Data-protection law generally expects technical measures appropriate to the data — and on a Postgres backend those measures have concrete names: row-level security, scoped policies, controlled keys and watched storage.

Boundaries first. This page is informational: it is not legal advice, it interprets no regulation, and it implies no compliance conclusion about any project. It maps a widely shared expectation — that only the right identities reach personal data — onto the mechanisms Supabase projects actually have.

The rest of the page walks through why access control carries so much weight when personal data is involved, a concrete technical posture to work toward on Supabase specifically, how to check where you stand today with queries you can run yourself, and how to keep the posture held between reviews.

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.

Why access control carries so much weight

Personal data in a typical product lives in ordinary tables — profiles, messages, events, addresses — and in less ordinary places: uploaded files in storage buckets, identifiers embedded in logs. A table that answers anonymous requests with its full contents undermines any posture document, whatever else is in place, because the exposure needs no exploit, only an anon key.

The principle doing the work here is least privilege: every identity can reach what it needs and nothing more. On Supabase the mechanism is row-level security — tables in the public schema protected by policies that grant rows to the authenticated identities that own or participate in them, and to nobody else.

Storage deserves equal billing. A bucket holding identity documents with a permissive policy leaks differently but just as completely as an unprotected table, and bucket policies are edited in the same console as table policies, by the same tired hands.

A concrete posture to work toward

The technical core is small enough to state plainly, and none of it is exotic. It amounts to a handful of properties, each expressible in one or two SQL statements and each independently checkable against the system catalog afterwards, which is what makes the posture demonstrable rather than merely intended.

Every public-schema table carries row-level security, enabled and forced. Policies are owner-scoped: reads filtered by the column expressing ownership, writes constrained with WITH CHECK, not merely USING. Storage buckets holding personal uploads use signed URLs rather than public access. Privileged keys stay server-side, and any key that reached browser-reachable code is rotated first and investigated second:

ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.documents FORCE ROW LEVEL SECURITY;

CREATE POLICY "owners_read_own_documents"
  ON public.documents
  FOR SELECT
  TO authenticated
  USING (owner_id = (SELECT auth.uid()));

CREATE POLICY "owners_write_own_documents"
  ON public.documents
  FOR INSERT
  TO authenticated
  WITH CHECK (owner_id = (SELECT auth.uid()));

Reading the posture critically

Tables are only half the surface. Storage buckets hold uploads and exports — frequently the most sensitive artifacts a product keeps — and they carry policies of their own, editable in the same console by the same hands. A permissive bucket policy is the unprotected-table problem wearing different clothes, so bucket visibility belongs beside the catalog query in any review.

The other quiet undo is key handling. A privileged key served to browsers bypasses every policy at once, which makes its location a posture question rather than a hygiene detail. Any credential of that class found in browser-reachable code warrants rotation first and investigation second — the order matters because analysis takes time and exposure does not wait.

Checking where you stand

The catalog answers the structural half of that question directly, without any special tooling. The query below is the same one RowShield itself runs — metadata only, never rows — and any row where rls_enabled reads false deserves understanding before anything else on this page:

Behaviour closes the loop, because configuration and reality routinely disagree. A table can carry the enabled flag while a tautological policy grants every row to everyone, or carry no policies at all and deny everything, breaking the app quietly instead of leaking loudly. Requesting your own API unauthenticated — the way RowShield's probe does, with GET requests only — shows which tables actually answer.

Keep the outputs dated whichever way you collect them, and keep them together in one place a colleague could find. A posture you can evidence over time reads very differently from one reconstructed after the fact, and reconstructing one under questioning is harder than either.

SELECT n.nspname AS schema,
       c.relname AS table_name,
       c.relrowsecurity AS rls_enabled,
       c.relforcerowsecurity AS forced
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
  AND n.nspname = 'public'
ORDER BY c.relname;

Keeping the posture held

Postures decay through ordinary work: a migration adds a table without the policy pair, a rename unhooks a predicate, a hotfix lands through the console and never reaches the repository. This is why RowShield treats the checks as ongoing rather than one-off — scans run on a schedule matching your plan, findings are recorded as created, regressed or resolved, and schema drift between scans is reported as appeared, came back or disappeared.

To be equally plain about the limits: RowShield automates none of the legal analysis, records none of the processing decisions, and produces nothing that resembles a compliance determination. It watches the database layer and says what changed. RowShield is an independent product, unaffiliated with and not endorsed by Supabase. Run a free audit at rowshield.dev/audit to see the technical half of your current posture.

Frequently asked

Does enabling RLS make a project compliant?
No single technical control determines compliance, and this page takes no position on any project's obligations. Row-level security is one widely expected technical measure for multi-user databases; assessments belong to qualified advisers looking at the whole system.
Is this page legal advice?
No. The page describes database mechanisms and a commonly recommended technical posture in general terms. Questions about legal requirements, processing records or regulator expectations need a qualified adviser.
Do storage buckets really matter for access-control posture?
Yes. Buckets frequently hold the most sensitive artifacts a product stores — uploads, attachments, exports — and a permissive bucket policy exposes them to whoever holds the public key, exactly as an unprotected table does.

Check your project in about ten seconds

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

Run the free audit
gdpr database access controlsupabase rls data protectiongdpr supabase securityleast privilege postgres