RowShield
Guides

The two-key mental model, once and for all

Supabase hands you keys with names that sound interchangeable and documentation that assumes you already know. The confusion is universal enough to deserve a reference page: two keys anchor the two ends of a trust spectrum, and almost every key-handling mistake traces to sliding something toward the wrong end.

This is a conceptual reference, maintained by hand: RowShield does not automate key management or rotate credentials. Where automation helps, it is in catching the failure mode this page explains: a service credential that wandered into client code.

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.

Two roles, two keys, one boundary

The anon key represents zero privilege: it identifies a caller who has not signed in, and every capability beyond browsing depends on database policies admitting the anon role. It is designed for publication — embedded in bundles, sent in headers, printed in quick-starts — because its only power is to invoke whatever RLS permits an unauthenticated caller.

The service_role key represents total privilege: it carries BYPASSRLS, so every policy in the project is decorative to whoever holds it, and every table answers as if the database trusted them completely, because the database does. No expiry, no scope, no second factor — the key is the authority. That asymmetry defines the handling rule: the anon key may appear anywhere; the service_role key appears only in server-side processes, environment variables, and secrets managers, and its appearance anywhere else is an incident.

Where authenticated users fit

Signed-in users confuse the model unnecessarily, because they introduce no third key. A signed-in browser still sends the same public key alongside an Authorization header carrying the user's access token; PostgREST reads the token's role claim — authenticated rather than anon — and evaluates policies against the user identity inside it. The public key remains the constant escort; the token varies per user and per session.

The practical consequence: three effective privilege levels, two of which travel together in every authenticated request. Policies distinguish anon from authenticated explicitly, and well-written ones further distinguish which authenticated user owns each row — the layering that makes per-user applications possible without per-user secrets.

Legacy JWTs and the newer key formats

Both classic keys ship as JWT-shaped strings whose payload announces the role: a decoded anon key claims the anon role, a decoded service_role key claims service_role. Newer projects increasingly issue self-describing formats instead — publishable keys prefixed sb_publishable_ taking the public slot and secret keys prefixed sb_secret_ taking the privileged one — with management tokens prefixed sbp_ belonging to the account plane and never to application traffic. The mental model survives format changes untouched: one class is safe to publish and bounded by policies, the other is authority itself. Classification matters practically because scanners, ours included, identify leaked credentials by decoding or prefix-matching — a leaked privileged key announces itself.

-- Shapes you will encounter (values synthetic):
eyJhbGciOi…REDACTED              -- legacy JWT; decode the payload's role claim
sb_publishable_…                 -- newer public key
sb_secret_…                      -- newer privileged key; server-side only
sbp_…                            -- management token; never in app code

Rules of thumb worth memorising

Prefix public variables honestly and keep privileged ones unprefixed, so framework bundling rules do the sorting for you. Grep your repository and your deployed bundles occasionally for the shapes above — cheap habit, expensive omission. When a server operation genuinely needs to bypass RLS, confine it to a route or function and keep the call out of anything shipped to browsers. And when an AI builder asks which key to use for an admin task, the answer is neither the client's nor the browser's: it belongs behind your server, full stop.

RowShield does not rotate or store keys — fingerprinting aside, credentials never persist in our systems. What it automates is detection of the specific catastrophe: the probe fetches your deployed pages and scripts, classifies credential-shaped strings, and reports any privileged key found in browser-reachable code as a critical finding, storing a fingerprint rather than the value. RowShield is an independent product, not affiliated with or endorsed by Supabase. Check your deployed site at rowshield.dev/audit — a URL is the only input.

Frequently asked

Is shipping the anon key in client code actually safe?
Yes — it is the intended deployment, and every Supabase quick-start embeds it. Its capabilities are bounded by Row Level Security, so a correctly policed database makes public key exposure a non-event.
I committed a service_role key to git. Is deleting it enough?
No. History, forks, caches and clones retain committed values indefinitely, so deletion is hygiene rather than remedy. Rotate the key immediately — invalidation is the only thing that retires a copied credential — then clean the repository at leisure.
Do authenticated users get their own key?
No. They present the same public key plus a per-user access token whose role claim elevates the session to authenticated. Policies then narrow what that particular identity may see, which is how per-user data stays separated without per-user secrets.

Check your project in about ten seconds

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

Run the free audit
supabase service_role vs anonsupabase api keys explainedwhich supabase key is secret