Security

    Is the Supabase service role key public?

    Supabase project API settings panel showing the anon publishable key alongside the masked service_role secret key with a warning label

    You spun up a Supabase project, copied two keys from the dashboard, and somewhere in the README it said the anon key is safe to ship to the browser. The question is whether the second key, the service_role one, can sit alongside it in your client bundle, or whether anything that touches the frontend has to leave that key alone. The honest answer matters because a large share of the leaked Supabase incidents reported in 2025 trace back to a single confusion between those two keys.

    Short answer

    No. The Supabase service_role key (and its replacement, the sb_secret_ secret key) is never public. It bypasses Row Level Security through the Postgres BYPASSRLS attribute, so anyone holding the key can read, modify, or delete every row in every table on the project. Only the anon key, or the new sb_publishable_ key, is designed to ship in client-side code. Supabase's official API keys documentation labels the secret category with the instruction: do not add it to web pages, public documents, source code, or bundles for mobile, desktop, or CLI apps. Treat the service_role key the way you would treat a database superuser password.

    What you should know

    • The service_role key holds admin-level database privileges. It maps to the service_role Postgres role, which has BYPASSRLS and ignores every Row Level Security policy on the project.
    • Supabase rolled out a new key system in June 2025. The new names are sb_publishable_ (safe to expose) and sb_secret_ (server only). Legacy anon and service_role JWTs remain valid during the transition and are scheduled for deletion in late 2026.
    • Secret keys are blocked from browser origins. Calling the REST or Realtime endpoints with an sb_secret_ key from a browser context returns HTTP 401, which is a server-side guardrail on top of the developer rule.
    • The most common leak pattern is a NEXT_PUBLIC_, VITE_, or EXPO_PUBLIC_ prefix. Any environment variable with one of those prefixes is inlined into the client bundle at build time, so the value ends up downloadable in a JS chunk.
    • GitHub push protection plus Supabase scanning auto-revokes leaked secret keys. The detection is fingerprint-based, so the prefixed sb_secret_ keys get caught reliably; legacy service_role JWTs are harder to spot.
    • Rotation is now zero-downtime. Supabase allows multiple secret keys in flight, so you can roll a replacement before deleting the compromised one.

    What does the service_role key actually do that the anon key cannot?

    The service_role key tells PostgREST, the Supabase REST gateway, to authenticate the request as the service_role Postgres role. That role is defined with the BYPASSRLS attribute, which means the Postgres planner skips Row Level Security policies entirely for the connection. A SELECT or DELETE issued under service_role returns or removes every row that matches the WHERE clause, regardless of policy.

    The anon key, by contrast, authenticates the same gateway as the anon Postgres role. That role has no BYPASSRLS attribute. Every query it issues is filtered by the RLS policies you wrote (or by Supabase's default-deny when RLS is enabled with no policy). The two keys hit the same endpoint and look superficially similar in the JWT structure, but the role they encode is the security boundary that separates a public client from a database administrator.

    The practical consequence is asymmetric. Exposing the anon key, with correct RLS, leaks no data beyond what an anonymous visitor was already allowed to see. Exposing the service_role key leaks everything in the database, including the auth schema where hashed passwords and refresh tokens live. Supabase's Securing your data guide puts the line plainly: secret and service role keys are never safe to expose because they bypass RLS.

    Why does the service_role key bypass Row Level Security in the first place?

    Because server-side code legitimately needs to do things the client cannot. A scheduled job that recomputes user balances, a webhook handler that writes an order on behalf of Stripe, a migration script that backfills a column: each of those tasks runs without a logged-in user and needs to act across rows belonging to many users. Forcing every one of those operations through RLS policies would mean either duplicating each policy in a permissive admin variant, or building a parallel set of database functions marked SECURITY DEFINER. Both are workable but heavier than a single BYPASSRLS role.

    Supabase chose the BYPASSRLS approach to keep server-side code ergonomic. The price of that choice is that the boundary between client and server is no longer enforced by RLS alone; it is enforced by where the service_role key lives. If the key sits only in environment variables read by a Node process, an Edge Function, or a server runtime, the boundary holds. If the key escapes to the client bundle, the boundary collapses.

    This is why the Supabase Row Level Security documentation treats the service_role key as a separate concern from RLS design. RLS protects you from the anon key being misused. Nothing in RLS protects you from the service_role key being misused, because the role's whole purpose is to skip those checks.

    What happens if the service_role key ends up in my frontend bundle?

    The key becomes a public database admin credential. Anyone who opens the browser developer tools, opens the Network tab, or scrapes the JS bundles served from the site can extract the key in seconds. Once extracted, it can be replayed against the project's REST endpoint with an HTTP client like curl or Postman, and every table in the public schema, plus the auth schema, becomes readable and writable.

    The failure mode is well-documented in 2026 vibe-coded apps because AI assistants frequently emit code along the lines of createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY) without questioning the prefix. The NEXT_PUBLIC_ prefix tells Next.js to inline the value into the JS bundle, so the build succeeds and the app appears to work; the leak stays invisible until someone audits the build.

    For builders who want an external automated read of a deployed web or mobile app before shipping it, PTKD.com (https://ptkd.com) is one of the platforms that scans compiled bundles and deployed origins for exposed Supabase secret-prefixed keys and visible BYPASSRLS roles. It catches the failure mode at upload time rather than after the dashboard fills up with rows from an attacker.

    How do Supabase's new sb_publishable and sb_secret keys change the picture?

    The new naming, announced in June 2025, solves three concrete problems. First, prefixes make role obvious at a glance: sb_publishable_ and sb_secret_ no longer require decoding a JWT to know what you are looking at. Second, the new keys are fingerprintable, so GitHub Secret Scanning, the Supabase 2025 security retrospective notes, can auto-revoke a leaked one. Third, rotation is now multi-key: you can issue a fresh secret key while the old one is still live, deploy it to production, and only then delete the old key.

    Propertyanon (legacy)sb_publishable_service_role (legacy)sb_secret_
    Intended locationBrowser, mobile bundleBrowser, mobile bundleBackend onlyBackend only
    Postgres roleanonanonservice_roleservice_role
    Bypasses RLSNoNoYes (BYPASSRLS)Yes (BYPASSRLS)
    Usable from browser originYesYesYes (legacy)No, HTTP 401
    Auto-revoked by GitHub scanningLimitedYesLimitedYes
    Zero-downtime rotationNoYesNoYes
    Status in late 2026Removed for restored projects since November 2025DefaultRemoved for restored projects since November 2025Default

    The two pairs share their Postgres role and their RLS behaviour. The improvement is operational: detection, rotation, and the 401 guardrail when an sb_secret_ key is called from a browser context.

    Where is the service_role key actually safe to use?

    In three places, all of them server-controlled. The first is an Edge Function (the Supabase term for a Deno-based serverless function running in the Supabase infrastructure), where the key is injected as an environment variable that never reaches a client. The second is a backend runtime under your control: a Node, Bun, Python, Go, or Rails process behind your own deployment that reads the key from a secret manager or a non-public environment variable. The third is short-lived administrative tooling: a local CLI script, a migration runner, or a one-off backfill job, run from a workstation and not committed to source control.

    The pattern that combines all three: the key never touches a build step that produces a client bundle, never lives in a variable whose name starts with NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_, or any other public-prefixed convention, and never appears in a log line shipped to a third-party log aggregator. If you cannot point to where the key lives at any given moment, it has probably leaked.

    What to watch out for

    The first trap is the AI code assistant that imports the service_role key on the client because the prompt did not specify which key to use. Half the AI-generated Supabase apps audited in 2025 by independent scanners used the service_role key in a place reachable from the browser. The fix is a single rule in the project's README and in the prompt template: the client always uses anon or sb_publishable_, the server always uses sb_secret_, never the reverse.

    The second trap is the developer who disables RLS to make the anon key behave like the service_role key for a feature that is hard to express in policy. The shortcut works for a sprint and then becomes the source of the next breach. RLS combined with a SECURITY DEFINER Postgres function is almost always the right structure when an anonymous caller legitimately needs to read or write data outside a simple per-user policy.

    The third trap is treating auto-revocation as protection. Supabase will revoke an sb_secret_ key that hits a public GitHub commit, but only after the key has been visible long enough for scrapers to copy it. Bots that watch the GitHub commit firehose can pull a fresh secret in seconds. A revoked key does not undo writes already issued against the database under that key.

    The fourth trap is the assumption that the service_role key only matters for the public schema. The same key reads the auth schema, including the hashed passwords and the email recovery tokens. A leaked service_role key is not only a data leak; it is a credential-recovery leak that can be turned into account takeover.

    Key takeaways

    • The Supabase service_role key is never public. It maps to a Postgres role with the BYPASSRLS attribute, so anyone holding the key can read or write every row in the database.
    • The anon key (or its sb_publishable_ replacement) is the one designed to ship to the browser, and only when Row Level Security is enabled with policies that match your data model.
    • Since June 2025 the new sb_secret_ keys are fingerprintable, browser-blocked, and rotatable without downtime; legacy service_role JWTs still work but are scheduled for deletion in late 2026.
    • The most reliable way to keep the service_role key safe is the operational rule that it never touches a build step producing a client bundle and never sits in an env var prefixed with NEXT_PUBLIC_, VITE_, or EXPO_PUBLIC_.
    • Some teams outsource the pre-submission scan to an external platform such as PTKD.com, which inspects compiled bundles and deployed origins for exposed Supabase secret-prefixed keys before the rest of the world finds them.
    • #supabase
    • #service role key
    • #anon key
    • #row level security
    • #rls
    • #ai-coded apps
    • #secrets

    Frequently asked questions

    Can I tell from a Supabase JWT alone whether it is the anon key or the service_role key?
    Yes. Decode the JWT at jwt.io and read the role claim. The anon key carries role anon, and the service_role key carries role service_role. The new system makes this easier still: keys prefixed with sb_publishable_ are safe to ship, keys prefixed with sb_secret_ are not. Treat any service_role JWT pasted into a Discord channel, a Notion doc, or a public repo as already compromised, because the role claim is visible to anyone who decodes the token.
    What should I do the minute I notice a service_role key in a public repository?
    Open the Supabase dashboard, go to Project Settings then API Keys, create a new secret key, swap it into your backend environment, then delete the old key. Keep both keys in rotation for as long as your slowest deploy needs. Then run git log -p for the value across the full history, and if the repo is public, rewrite history or treat the database as breached and audit recent rows for unauthorised changes.
    Does the anon key being public mean I can ignore Row Level Security?
    No. The anon key carries the anon Postgres role, which has no BYPASSRLS attribute, so every query is filtered by your RLS policies. With RLS disabled on a table, the same anon key becomes a public handle to every row. Supabase now enables RLS by default for new tables and emails a warning when one is created without policies attached, but writing correct policies still sits with the developer.
    Can I keep the service_role key in a Vercel environment variable safely?
    Yes, if it stays on the server. Vercel marks each environment variable as available either to the build output, to Server Actions and API routes, or to both. Any variable prefixed with NEXT_PUBLIC_ is inlined into the client bundle at build time, so a name like NEXT_PUBLIC_SUPABASE_SERVICE_KEY would defeat the protection. Keep the value under a server-only name and read it from a route handler, an edge function, or a Server Action.
    Will my service_role key be auto-revoked if I push it to GitHub?
    For newer sb_secret_ keys, yes. Supabase joined the GitHub Secret Scanning Partner Program, so when a secret-prefixed key lands in a public repository, GitHub notifies Supabase and Supabase revokes the key automatically. Legacy service_role JWTs are harder to fingerprint, so coverage is best-effort. Treat auto-revocation as a safety net for honest accidents, not a substitute for keeping the key out of version control and out of client-side environment variables to begin with.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free