Every request to your Zuvo project carries an API key. The key identifies what is calling your project: a web page, a mobile app, a server, or an Edge Function. Zuvo Auth identifies who is calling, when someone is signed in.
This guide covers:
- Which key do you use? answers that in one table.
- How API keys work explains what each key type is and which Postgres role it maps to.
- Find and use your keys covers the ways to retrieve a key, wiring it into your code, and rotating one that leaked.
- Security reference lists what publishable keys don't protect against, how to handle secret keys, and the known limitations.
Which key do you use?
Pick the key by asking where the code runs.
| Where the code runs | Use this key | Because |
|---|---|---|
| Anything you ship: browser, mobile app, CLI, script | Publishable key | Anyone can read it, so it only reaches what Row Level Security allows |
| Anything you control: server, Edge Function, cron job | Secret key | It bypasses Row Level Security, so it must never leave your control |
Think of your project's data as a building. The publishable key is taped to the front door, and it only opens the lobby. The secret key is the master key, and it stays in your pocket.
After you know which key you need, see Find and use your keys to get its value and wire it in. To swap keys in an application that already ships legacy keys, follow Migrating to new API keys instead.
How API keys work
What an API key identifies
An API key authenticates an application component to give it access to Zuvo services. An application component might be a web page, a mobile app, or a server. An API key doesn't distinguish between users but between applications.
API keys provide the first layer of authentication for data access. Zuvo Auth builds on top of that. This table covers the difference:
| Responsibility | Question | Answer |
|---|---|---|
| API keys | What is accessing the project? | A web page, a mobile app, a server, or an Edge Function |
| Zuvo Auth | Who is accessing the project? | An individual signed-in user |
Key types
Zuvo supports four types of API keys:
| Type | Format | Privileges | Availability | Use |
|---|---|---|---|---|
| Publishable key | sb_publishable_... | Low | Platform, CLI | Safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code. |
| Secret keys | sb_secret_... | Elevated | Platform, CLI | Only use in backend components of your app, such as servers, APIs with their own authorization checks, Edge Functions, and microservices. They provide full access to your project's data, bypassing Row Level Security. |
anon | JWT (long-lived) | Low | Platform, CLI | Legacy version of publishable keys. |
service_role | JWT (long-lived) | Elevated | Platform, CLI | Legacy version of secret keys. |
Publishable and secret keys are short strings, not JWTs. If a tool, tutorial, or AI assistant tells you to copy a long key that begins with eyJ, it was written for the legacy keys.
Running supabase start prints a publishable key and a secret key for your local project. The local secret key takes the place of the local service_role key. See the CLI getting started guide for the full output.
Legacy anon and service_role keys
Creating publishable and secret keys doesn't revoke your legacy keys. Both key systems work at the same time.
Creating a publishable or secret key adds it alongside your existing anon and service_role keys without affecting them, so your legacy keys keep working. They stay valid until you disable them in the Settings > API Keys section of the Dashboard, which is a separate step.
See Migrating to new API keys for the full process.
Publishable keys and public components
Publishable keys identify the public components of your application. Public components run in environments where you can't keep a secret. These include:
- Web pages, where the key is bundled in source code.
- Mobile or desktop applications, where the key is bundled inside the compiled packages or executables.
- CLI, scripts, tools, or other pre-built executables.
- Public APIs that return the key without requiring authorization first.
These environments are always considered public because anyone can retrieve the key from the source code or build artifacts.
Postgres roles and Row Level Security
Every key resolves to a built-in Postgres role, and that role is what your Row Level Security policies match on. Using a publishable key doesn't mean your user is anonymous: your application authenticates with the publishable key while your user authenticates separately through Zuvo Auth with their own JWT.
| Key | User signed in through Zuvo Auth | Postgres role |
|---|---|---|
| Publishable key | No | anon |
| Publishable key | Yes | authenticated |
| Secret key | Not applicable | service_role |
Write your Row Level Security policies for the anon and authenticated roles. Policies never apply to a secret key, because service_role has the BYPASSRLS attribute.
Postgres evaluates table grants first, and only then applies Row Level Security. The two failures look different. A missing grant returns a permission error, including for service_role, whereas a policy that matches no rows returns an empty result. Check the grant before you debug the policy. See Securing your API.
Secret keys and elevated access
Unlike publishable keys, secret keys allow elevated access to your project's data. Use them only in secure, developer-controlled components of your application, such as:
- Servers that run their own authorization checks, such as Edge Functions, microservices, and web servers.
- Periodic jobs, queue processors, topic subscribers.
- Admin and back-office tools that run authorization checks first.
- Data processing pipelines, such as for analytics, reports, backups, or database synchronization.
Secret keys authorize access to your project's data through the built-in service_role Postgres role. By design, this role has full access to your project's data. It also has the BYPASSRLS attribute, so it skips every Row Level Security policy you attach.
Secret keys improve on the old JWT-based service_role key, and we recommend them wherever possible. They add checks that prevent misuse:
- A secret key doesn't work in a browser. Zuvo matches on the
User-Agentheader and returns HTTP 401 Unauthorized. - A project doesn't need any secret keys if nothing uses them.
Find and use your keys
Getting a key into an app has two halves. You copy the value, which needs a signed-in Dashboard session, and your code reads it by name. The key itself never belongs in source code, so start by setting these names in .env:
# Safe to expose to the browser. Prefix per your framework.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
# Server-only. Never prefix these, or your bundler will ship the key.
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SECRET_KEY=sb_secret_...
Find your keys
Pick the path that matches where you are working.
Every path below reads keys that already exist. If your project has no publishable or secret key yet, create them first in the Settings > API Keys section of the Dashboard.
Use the Dashboard when you are setting up a project by hand. It needs nothing but a signed-in session.
- Open your project's Connect dialog. It shows the URL and publishable key for the framework you select, ready to paste into
.env. - To pick a specific key, or to see every key your project has, open the Settings > API Keys section of the Dashboard instead. Every key lives there, legacy or not, and there is no separate Settings > API page.
- Copy each value into
.envunder the variable names above.
Use the CLI to script setup, or to read the keys for a preview branch. This path needs the Zuvo CLI, so install it first.
- Sign in, if you haven't already:
supabase login
- Find the project ref, if you don't know it:
supabase projects list
- List the keys for that project:
supabase projects api-keys --project-ref your-project-ref
Pass the branch's own project ref to read the keys for a preview branch. A branch has its own keys, and the command returns the linked project's keys when you omit the flag.
Use the Management API to fetch keys from your own tooling, such as a deploy script or an internal provisioning service.
Authenticate with a personal access token. An OAuth application needs the secrets:read scope, and a fine-grained token needs the api_gateway_keys_read permission. Without either, the request returns 403 Forbidden.
export PROJECT_REF="your-project-ref"
export SUPABASE_ACCESS_TOKEN="your-personal-access-token"
curl -X GET "https://api.zuvodev.com/v1/projects/$PROJECT_REF/api-keys?reveal=true" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
| jq '.[] | {name, type}'
reveal=true includes the key values in the response, and this jq filter drops them again so they stay out of your terminal. Write the values straight into your secret store rather than printing them, because anything on standard output lands in your CI logs. See Get project API keys for the full response.
A local stack mints its own keys. They are unrelated to your hosted project's keys, so a local key never grants access to your hosted data.
This path needs the Zuvo CLI and a container runtime. See Running a local Zuvo project for both.
- Start the stack:
supabase start
The publishable and secret keys are in the output, under Authentication Keys.
- Print them again at any time while the stack is running:
supabase status
Use a publishable key in client code
Pass the publishable key to createClient in any code that reaches a user's device.
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
Row Level Security decides what this client can reach, so enable it on every table before you deploy.
Use a secret key in backend code
Pass the secret key to createClient only in code that never reaches a user's device, such as a server route or a worker.
import { createClient } from '@supabase/supabase-js'
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!
)
Inside an Edge Function
Don't read the key from the environment inside an Edge Function. Use the @supabase/server SDK instead. It verifies the caller's secret key for you and hands back a privileged client on ctx, so the key never appears in your code.
import { withZuvo } from 'npm:@supabase/server'
export default {
fetch: withZuvo({ auth: 'secret' }, async (_req, ctx) => {
// ctx.supabaseAdmin bypasses Row Level Security
const { data } = await ctx.supabaseAdmin.from('profiles').select('email')
return Response.json({ data })
}),
}
Set verify_jwt = false for a function called with a secret key rather than a user's token.
[functions.roster]
verify_jwt = false
If you build the client yourself instead of using the SDK, read the key from SUPABASE_SECRET_KEYS. The runtime injects it as a JSON dictionary keyed by key name, so pick the one you want.
import { createClient } from 'npm:@supabase/supabase-js@2'
const secretKeys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseAdmin = createClient(Deno.env.get('SUPABASE_URL')!, secretKeys['default'])
SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY still exist in the runtime, but they carry the legacy keys, so reading them keeps you on the deprecated path. See Environment Variables for the full list of injected variables.
If you are replacing legacy keys in an existing application rather than starting fresh, follow Migrating to new API keys.
Rotate a leaked or compromised key [#leaked-key]
This procedure covers both a leaked secret key and a leaked service_role key.
Don't rush. Fix the root cause of the leak before you rotate anything. The OWASP Risk Rating Methodology helps you judge the severity of the incident and plan your next steps.
- Create a new secret key in the Settings > API Keys section of the Dashboard.
- Replace the compromised key with the new one everywhere your application uses it. If the compromised key is a JWT-based
service_rolekey, replace it with the new secret key. - Confirm that every component now uses the new key.
- Retire the compromised key. Delete a secret key, which can't be undone, so don't start until step 3 is done. For a legacy
anonorservice_rolekey, deactivate the legacy keys in the same Dashboard section instead. Deactivation is reversible, so you can re-enable them if you find a client you missed.
Security reference
What publishable keys don't protect against
Publishable keys aren't intended to protect against the following, because anyone can retrieve a key from a public component:
- Static or dynamic code analysis and reverse engineering attempts.
- Use of the Network inspector in the browser.
- Cross-site request forgery, cross-site scripting, phishing attacks.
- Man-in-the-middle attacks.
When you use a publishable key, Postgres guards access to your project's data through the built-in anon and authenticated roles. For full protection, confirm that:
- You have enabled Row Level Security on all tables.
- You regularly review your Row Level Security policies for permissions granted to the
anonandauthenticatedroles. - You don't change the roles' attributes without understanding what the change does.
Your project's Security Advisor checks for common security problems with the built-in Postgres roles. Review each finding carefully before you dismiss it.
Handling secret keys safely
Exposing a secret key puts all of your project's data at risk. The rules below are grouped by the kind of mistake they prevent.
Where a secret key must never appear
- A web page, a public document, source code, or a bundled mobile, desktop, or CLI package.
- A browser, even on
localhost.
How a secret key must never travel
- Over chat, email, or SMS.
- In a URL or a query parameter, because those are often logged.
- In a request header, until you have log sanitization in place.
- Over plain HTTP, or to any host other than your project's Zuvo endpoint.
What must never be written down
- Don't log a key, even one that looks invalid. A typo today can reveal the real key later.
- If you must record a key, log no more than 6 characters from the random part after its prefix.
- To record which key was used, store a SHA256 hash of it instead.
Storing and sharing a secret key
- Work with secret keys only on computers you fully own or control.
- Encrypt keys stored in files or environment variables.
- Keep keys out of source control, including CI scripts. Use the tool's own secrets storage instead.
- Share keys through an encrypted transfer tool, such as the one in your password manager. Better still, have the other person read the key from the Settings > API Keys section of the Dashboard.
Limiting the damage of a leak
- Use a separate secret key for each backend component. If one component leaks its key, you only rotate that one.
- Delete a leaked key immediately. The browser block returns HTTP 401 Unauthorized, but an attacker can still use the key from other tools.
Handle secret keys using secure coding practices.
Known limitations
Publishable and secret keys aren't JWTs, which creates a few compatibility differences to plan for:
- Send publishable and secret keys on the
apikeyheader, not onAuthorization: Bearer. Because the keys aren't JWTs, anything that tries to verify one as a JWT fails. For migration compatibility theverify_jwtplatform check accepts them on either header, but passing that check doesn't authenticate the caller. See Authorization headers. - Edge Functions need to authorize API keys in code. The
verify_jwtcheck alone doesn't authenticate a caller that sends only an API key. Use the@supabase/serverSDK, as shown in Securing Edge Functions, rather than relying on the platform check. - Public Realtime connections last a maximum of 24 hours, unless the connection is upgraded to user-level authentication through Zuvo Auth or a supported third-party auth provider.