RowShield
Guides

Edge functions hold service_role — verify the boundary actually holds

Supabase edge functions exist partly so that service_role keys have somewhere legitimate to live. The key carries BYPASSRLS — every policy in your project is decorative to whoever holds it — and functions run server-side, receive secrets through environment bindings, and expose only an HTTPS endpoint. Used as intended, the architecture is sound. The phrase doing heavy lifting in that sentence is "as intended", because boundaries hold by construction only until something small erodes them.

This page treats the boundary as a claim to verify rather than an assumption to trust: the specific ways it erodes in real projects, the checks that detect erosion, and the rotation order for the day verification fails. The SERVICE_ROLE_KEY_EXPOSED probe backs the automated part — RowShield fetches your deployed frontend the way any visitor would and inspects what actually ships, because the question is never where the key is supposed to be, only where it is.

Rules that check this

The right home for a bypass credential

Understanding why the key is catastrophic in the wrong place clarifies why functions are the right place. BYPASSRLS skips policy evaluation wholesale: reads, writes, everything, on every table, as no identity at all. A leaked anon key is bounded by your policies; a leaked service_role key is bounded by nothing except network reachability. Two keys with superficially similar formats and categorically different power.

Edge functions keep that power server-side: the secret arrives via environment binding at runtime, appears in no bundle, and the function mediates which database operations happen. The design contract is that clients invoke the function, never the database directly, for anything administrative. Most projects honour the contract at birth. Boundaries erode through convenience, not decision — which is why verification beats intention.

How the boundary erodes

Four erosions recur. Debugging moves a call client-side temporarily — the service key pasted into frontend code to test a theory — and temporary code ships. A shared module exports a pre-configured admin client and something imports it from a component rendered in the browser. Environment plumbing misroutes: a variable prefixed for public exposure receives the wrong value, and build inlining bakes it into static assets. Or a function itself is wide open — token verification disabled during testing and never restored — so anyone can invoke the privileged endpoint anonymously.

Each erosion shares a property: nothing errors. The calls succeed, features work, dashboards stay green. The boundary fails silently in exactly the way that makes scheduled external verification worthwhile — the failure mode is invisible from inside the codebase precisely because it consists of code being somewhere it should not be, which only a view of the deployed artifact reveals.

Verifying from outside

External verification asks the browser's question: what does this deployment actually serve? RowShield's probe fetches the page, follows same-origin scripts and module preloads, and scans for Supabase credential formats — legacy JWTs classified by decoding the role claim, sb_secret_ and sbp_ tokens, publishable keys noted as expected. Findings record fingerprints, never the credential itself. From inside, the complementary check is blast-radius awareness: knowing what the key could touch if it escaped, which is one query:

Treat the blast-radius list as the honest answer to the question asked after incidents: what could they have taken? Every row marked RLS OFF was reachable without any credential at all; the rest became reachable wholesale the moment the key escaped. The list tends to motivate the cleanup conversation better than any diagram.

SELECT n.nspname AS schema,
       c.relname AS table_name,
       CASE WHEN c.relrowsecurity THEN 'rls on' ELSE 'RLS OFF' END AS posture
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.relrowsecurity, c.relname;

If the boundary failed: rotation order

Rotation precedes investigation, because a bypass credential in unknown hands makes everything else theatre. Rotate in the dashboard first — every dependent call breaks at once, which is the desired inventory of what used the key. Restore each caller to the function-mediated path, restore verification on any function left open, and only then resume normal operation. The function route exists so this list is short; if the list is long, the architecture drifted too.

Ongoing, the probe re-runs on schedule so erosion is caught as a transition: a deployment gaining a service_role credential raises a critical alert naming the finding, and resolution is confirmed by the same external view that detected it. RowShield is an independent product, unaffiliated with and not endorsed by Supabase; the probe issues GET, HEAD and OPTIONS only, refuses loopback and private address ranges before connecting, and stores keys as fingerprints — six characters and a length — never values.

Frequently asked

Is putting service_role in edge function environment variables safe?
Yes — that is the intended pattern: the secret binds to a server runtime, never enters a bundle, and the function mediates access. Safety depends on the boundary staying intact, which is a property to verify against the deployed artifact rather than assume from the source, since only deployment reveals what actually ships.
Does token verification on the function make it safe by itself?
Verification confirms a caller holds a valid Supabase JWT — which every visitor, including anonymous ones, can obtain. It authenticates, it does not authorise; a function performing privileged work still needs its own logic distinguishing which callers may trigger which operations, or verified-anyone remains effectively everyone.
Rotate or delete an exposed service_role key?
Rotate, immediately, before investigating — deletion breaks callers without closing the window on copies already taken. Rotation invalidates the leaked value while handing you, through the breakage, the complete list of places the key was wired. Investigation follows; ordering matters more than speed after the rotation is done.

Check your project in about ten seconds

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

Run the free audit
supabase edge function service roleservice_role key edge functionsupabase function secrets exposurerotate exposed supabase service_role