mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: add the missing API key decision information (#49799)
Closes DOCS-1311 Closes FDBKIN-2926 Closes DOCS-694 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Docs update. Corrections and new content. This is the PR that is to bring the Eval to green. ## What is the current behavior? Two statements are wrong, and the gaps behind most logged confusion about this page are unfilled. - The Availability column marks publishable and secret keys Platform-only. `supabase start` prints both. - The page says Edge Functions only verify the legacy keys and to use `--no-verify-jwt`. #49700 updated `guides/functions/auth-headers` to document that `verify_jwt` accepts the new keys on either header, but left this page and the migration guide stating the old behavior. - The page has no code samples, so it never shows how a key reaches code. An agent reading it falls back on `SUPABASE_SERVICE_ROLE_KEY`, the legacy key this same page deprecates. - Nothing maps `anon` and `service_role` to their replacements, or says the replacements aren't `eyJ`-prefixed JWTs. - The Postgres role table covers only publishable keys. ## What is the new behavior? Corrections: - Mark all four key types available on Platform and CLI, and note that the local secret key takes the place of the local `service_role` key. - Point the Edge Functions guidance at the `@supabase/server` SDK instead of `--no-verify-jwt`. Fix the same bullet in the migration guide. Additions: - "Coming from `anon` and `service_role`" gives the legacy-to-new mapping and says the replacements aren't JWTs. - Extend the Postgres role table to cover secret keys, and note that grants are evaluated before Row Level Security, so a missing grant fails even for `service_role`. - State who does what. Copying a key needs a signed-in Dashboard session, so it is a person's step, while code only refers to the variable name. Add a `.env` sample naming the variables. - Add the two `createClient` samples the page lacked, plus an "Inside an Edge Function" subsection using `withSupabase`, which reads no key from the environment. - Cross-reference from the key decision to retrieving a value, wiring it into code, or migrating an application that ships legacy keys. ## Additional context PR 4 of 4. Base is #49797. ## Manual testing 1. Open the API keys guide on the deploy preview. 2. Check the Key types table. All four rows read "Platform, CLI". 3. Check Known limitations. It no longer mentions `--no-verify-jwt`. 4. Open the migration guide and check Known limitations. The Edge Functions bullet matches. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated API key guidance with clearer instructions for finding, selecting, and using publishable and secret keys. * Added examples for environment variables, client applications, backend code, and Edge Functions. * Clarified key formats, CLI availability, local development output, Postgres role mappings, and authorization behavior. * Expanded guidance on `apikey` headers, RLS errors, and Edge Function API key authorization. * Refined migration guidance for API key authentication in Edge Functions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
5bd0b90cf0
commit
4d2bd0eacf
2 files changed
+101
-15
No files matched your search
@@ -10,7 +10,7 @@ This guide covers:
|
||||
|
||||
- [Which key do you use?](#which-key-do-you-use) answers that in one table.
|
||||
- [How API keys work](#how-api-keys-work) explains what each key type is and which Postgres role it maps to.
|
||||
- [Find and use your keys](#find-and-use-your-keys) covers the ways to retrieve a key and how to rotate one that leaked.
|
||||
- [Find and use your keys](#find-and-use-your-keys) covers the ways to retrieve a key, wiring it into your code, and rotating one that leaked.
|
||||
- [Security reference](#security-reference) lists what publishable keys don't protect against, how to handle secret keys, and the known limitations.
|
||||
|
||||
## Which key do you use?
|
||||
@@ -24,7 +24,7 @@ Pick the key by asking where the code runs.
|
||||
|
||||
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.
|
||||
|
||||
With the key chosen, go to [Find and use your keys](#find-and-use-your-keys) to get its value, or to [How API keys work](#how-api-keys-work) for what that key authorizes and which Postgres role it maps to.
|
||||
After you know which key you need, see [Find and use your keys](#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](/docs/guides/getting-started/migrating-to-new-api-keys) instead.
|
||||
|
||||
<$Partial path="api_keys_deprecation.mdx" />
|
||||
|
||||
@@ -47,11 +47,15 @@ Supabase supports four types of API keys:
|
||||
|
||||
| Type | Format | Privileges | Availability | Use |
|
||||
| ---------------------------------------------------------- | ---------------------------------------------------------------- | ---------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Publishable key | <span className="whitespace-nowrap!">`sb_publishable_...`</span> | Low | Platform | Safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code. |
|
||||
| Secret keys | <span className="whitespace-nowrap!">`sb_secret_...`</span> | Elevated | Platform | Only use in backend components of your app, such as servers, APIs with their own authorization checks, [Edge Functions](/docs/guides/functions), and microservices. They provide full access to your project's data, bypassing [Row Level Security](/docs/guides/database/postgres/row-level-security). |
|
||||
| Publishable key | <span className="whitespace-nowrap!">`sb_publishable_...`</span> | Low | <span className="whitespace-nowrap!">Platform, CLI</span> | Safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code. |
|
||||
| Secret keys | <span className="whitespace-nowrap!">`sb_secret_...`</span> | Elevated | <span className="whitespace-nowrap!">Platform, CLI</span> | Only use in backend components of your app, such as servers, APIs with their own authorization checks, [Edge Functions](/docs/guides/functions), and microservices. They provide full access to your project's data, bypassing [Row Level Security](/docs/guides/database/postgres/row-level-security). |
|
||||
| <span className="whitespace-nowrap!">`anon`</span> | JWT (long-lived) | Low | <span className="whitespace-nowrap!">Platform, CLI</span> | Legacy version of publishable keys. |
|
||||
| <span className="whitespace-nowrap!">`service_role`</span> | JWT (long-lived) | Elevated | <span className="whitespace-nowrap!">Platform, CLI</span> | 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](/docs/guides/local-development/cli/getting-started) 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.
|
||||
@@ -73,12 +77,17 @@ These environments are always considered public because anyone can retrieve the
|
||||
|
||||
### Postgres roles and Row Level Security
|
||||
|
||||
Using a publishable key doesn't mean your user is anonymous. Your application authenticates with the publishable key while your user authenticates separately through Supabase Auth with their own JWT:
|
||||
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 Supabase Auth with their own JWT.
|
||||
|
||||
| Key | User logged in via Supabase Auth | Postgres role |
|
||||
| --------------- | -------------------------------- | --------------- |
|
||||
| Publishable key | No | `anon` |
|
||||
| Publishable key | Yes | `authenticated` |
|
||||
| Key | User signed in through Supabase 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](/docs/guides/api/securing-your-api#grants-and-rls).
|
||||
|
||||
### Secret keys and elevated access
|
||||
|
||||
@@ -104,6 +113,18 @@ Secret keys improve on the old JWT-based `service_role` key, and we recommend th
|
||||
|
||||
## 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`:
|
||||
|
||||
```bash .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.
|
||||
@@ -122,8 +143,8 @@ Every path below reads keys that already exist. If your project has no publishab
|
||||
Use the Dashboard when you are setting up a project by hand. It needs nothing but a signed-in session.
|
||||
|
||||
1. Open your project's [**Connect** dialog](/dashboard/project/_?showConnect=true). It shows the URL and publishable key for the framework you select, ready to paste into `.env`.
|
||||
2. To pick a specific key, or to see every key your project has, open the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard instead.
|
||||
3. Copy each value into your project's `.env` file.
|
||||
2. To pick a specific key, or to see every key your project has, open the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard instead. Every key lives there, legacy or not, and there is no separate **Settings > API** page.
|
||||
3. Copy each value into `.env` under the variable names above.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="cli" label="Supabase CLI">
|
||||
@@ -192,6 +213,71 @@ This path needs the Supabase CLI and a container runtime. See [Running a local S
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Use a publishable key in client code
|
||||
|
||||
Pass the publishable key to `createClient` in any code that reaches a user's device.
|
||||
|
||||
```ts lib/supabase.ts
|
||||
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.
|
||||
|
||||
```ts server/supabase.ts
|
||||
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`](/docs/guides/functions/auth) 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.
|
||||
|
||||
```ts supabase/functions/roster/index.ts
|
||||
import { withSupabase } from 'npm:@supabase/server'
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ 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.
|
||||
|
||||
```toml supabase/config.toml
|
||||
[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.
|
||||
|
||||
```ts supabase/functions/roster/index.ts
|
||||
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](/docs/guides/functions/secrets) 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](/docs/guides/getting-started/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.
|
||||
@@ -262,6 +348,6 @@ Handle secret keys using [secure coding practices](https://owasp.org/www-project
|
||||
|
||||
Publishable and secret keys aren't JWTs, which creates a few compatibility differences to plan for:
|
||||
|
||||
- You can't send a publishable or secret key in the `Authorization: Bearer ...` header unless the value exactly matches the `apikey` header. Supabase forwards that request to your project's database, which rejects it because the value isn't a JWT.
|
||||
- Edge Functions only support JWT verification through the `anon` and `service_role` JWT-based API keys. Use the `--no-verify-jwt` option with publishable and secret keys. The Supabase platform doesn't verify the `apikey` header for Edge Functions called this way, so implement your own `apikey` authorization inside the function.
|
||||
- Send publishable and secret keys on the `apikey` header, not on `Authorization: Bearer`. Because the keys aren't JWTs, anything that tries to verify one as a JWT fails. For migration compatibility the `verify_jwt` platform check accepts them on either header, but passing that check doesn't authenticate the caller. See [Authorization headers](/docs/guides/functions/auth-headers).
|
||||
- Edge Functions need to authorize API keys in code. The `verify_jwt` check alone doesn't authenticate a caller that sends only an API key. Use the `@supabase/server` SDK, as shown in [Securing Edge Functions](/docs/guides/functions/auth), 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 Supabase Auth or a supported third-party auth provider.
|
||||
@@ -210,8 +210,8 @@ Once you've confirmed nothing uses the legacy keys, deactivate them in the [**Se
|
||||
|
||||
A few behaviors differ from the legacy JWT-based keys. Plan for them during the migration:
|
||||
|
||||
- You can't send a publishable or secret key in the `Authorization: Bearer ...` header. Send it on the `apikey` header instead.
|
||||
- Edge Functions don't verify the `apikey` header for the new keys. Use `verify_jwt = false` and authorize in code, as shown in [Step 4](#step-4-update-edge-functions).
|
||||
- Send publishable and secret keys on the `apikey` header. For migration compatibility the `verify_jwt` check also accepts them on `Authorization: Bearer ...`, but passing that check doesn't authenticate the caller, and the keys aren't JWTs, so nothing downstream can verify them as one.
|
||||
- Edge Functions need to authorize API keys in code. The `verify_jwt` platform check alone doesn't authenticate a caller that sends only an API key, so authorize the key in your handler, as shown in [Step 4](#step-4-update-edge-functions).
|
||||
- Public Realtime connections are limited to 24 hours unless the connection is upgraded with user-level authentication through Supabase Auth or a supported third-party auth provider.
|
||||
|
||||
## Next steps
|
||||
|
||||
Reference in new issue
Block a user