diff --git a/apps/docs/content/guides/getting-started/api-keys.mdx b/apps/docs/content/guides/getting-started/api-keys.mdx index 78be04b482c..2b4218f3aa2 100644 --- a/apps/docs/content/guides/getting-started/api-keys.mdx +++ b/apps/docs/content/guides/getting-started/api-keys.mdx @@ -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 | `sb_publishable_...` | Low | Platform | Safe to expose online: web page, mobile or desktop app, GitHub actions, CLIs, source code. | -| Secret keys | `sb_secret_...` | 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 | `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](/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). | | `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](/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. @@ -192,6 +213,71 @@ This path needs the Supabase CLI and a container runtime. See [Running a local S +### 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. diff --git a/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx b/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx index 089b1b3f3f4..66bec46fce7 100644 --- a/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx +++ b/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx @@ -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