--- id: 'auth' title: 'Securing Edge Functions' description: 'Authentication patterns for Supabase Edge Functions.' subtitle: 'Authentication patterns for Edge Functions' --- Secure an Edge Function by declaring which credentials it accepts. The `withSupabase` wrapper from the [`@supabase/server` package](https://github.com/supabase/server) on GitHub checks each caller against the `auth` mode you set. Your handler receives a preconfigured Supabase client on `ctx`. For how authorization headers and the `verify_jwt` platform check work, see [Authorization headers](/docs/guides/functions/auth-headers). The wrapper accepts four `auth` modes: | Mode | Accepts | | --------------- | ------------------------------------------ | | `'user'` | A valid user JWT on `Authorization` | | `'secret'` | A secret key on `apikey` | | `'publishable'` | A publishable key on `apikey` | | `'none'` | Any caller, no check (for signed webhooks) | ## Authenticated user calls When a signed-in user calls a function, the request carries the user's session JWT on the `Authorization` header. Your app usually makes that call through `supabase.functions.invoke`. The default is `verify_jwt = true`. The platform validates the JWT before your handler runs. Use `auth: 'user'` to get a `ctx.supabase` scoped to the caller's Row Level Security (RLS) policies. ```ts import { withSupabase } from 'npm:@supabase/server@1' export default { fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { const { supabase, supabaseAdmin, userClaims, jwtClaims, authMode } = ctx // supabase — RLS-scoped to the authenticated user // supabaseAdmin — bypasses RLS (service role) // userClaims — user identity from JWT (id, email, role) // jwtClaims — full JWT claims // authMode — which auth mode matched // your business logic goes here return Response.json({ email: ctx.userClaims?.email }) }), } ``` ## Service-to-service calls Cron jobs, workers, `pg_net`, and other Edge Functions make calls with a secret key on the `apikey` header rather than a user JWT. Disable `verify_jwt` and use `auth: 'secret'`. The wrapper validates the key against any secret key in your [project's API keys](/dashboard/project/_/settings/api-keys), and gives your handler `ctx.supabaseAdmin` for privileged work. ```ts import { withSupabase } from 'npm:@supabase/server@1' export default { fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => { // your business logic. ctx.supabaseAdmin bypasses RLS return Response.json({ ok: true }) }), } ``` To accept only one specific key, use `auth: 'secret:'`. For example, `auth: 'secret:automations'` accepts only the secret key named `automations`. To name a key, open [**Settings > API keys**](/dashboard/project/_/settings/api-keys) in the Supabase Dashboard. The same syntax works for publishable keys: `auth: 'publishable:'`. ![The Secret keys section of the Supabase Dashboard. A table with Name and API key columns lists two keys, "default" and "automations". Each row shows a masked sb_secret_ value with reveal and copy buttons.](/docs/img/guides/functions/secret-keys-automations.png) ## Public functions For a genuinely public function, such as a health check, use `auth: 'none'` with `verify_jwt = false` so anonymous callers can reach the handler. ```toml [functions.health] verify_jwt = false ``` ```ts import { withSupabase } from 'npm:@supabase/server@1' export default { fetch: withSupabase({ auth: 'none' }, async () => { // your business logic return Response.json({ ok: true }) }), } ``` `auth: 'none'` accepts every caller. For a function that authenticates callers itself, see the [External webhooks](#external-webhooks) section of this page. ## External webhooks External providers such as Stripe or GitHub don't send Supabase credentials. They sign the request body with their own shared secret. Use `auth: 'none'` to skip the wrapper's credential check, then verify the provider's signature inside the handler. Keep `verify_jwt = false`. ```ts import { withSupabase } from 'npm:@supabase/server@1' import Stripe from 'npm:stripe' const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!) // Deno has no synchronous Node crypto, so signature verification must go through // Stripe's SubtleCryptoProvider. The synchronous `constructEvent()` throws // "SubtleCryptoProvider cannot be used in a synchronous context" on this runtime. const cryptoProvider = Stripe.createSubtleCryptoProvider() export default { fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { const signature = req.headers.get('stripe-signature') ?? '' const body = await req.text() try { await stripe.webhooks.constructEventAsync( body, signature, Deno.env.get('STRIPE_WEBHOOK_SECRET')!, undefined, cryptoProvider ) } catch (err) { // Log the reason so a configuration error isn't mistaken for a forged payload. console.error('Stripe signature verification failed:', err) return new Response('bad signature', { status: 400 }) } // your business logic. ctx.supabaseAdmin available for database work return Response.json({ received: true }) }), } ``` `auth: 'none'` disables every credential check, so your handler is fully responsible for authenticating the caller. When the function reads or writes sensitive data, verify the caller yourself inside the handler. ## Combining modes Functions that answer both users and internal callers take an array on `auth`. The wrapper tries each mode in order and uses the first one that matches. `ctx.authMode` tells you which mode matched. ```ts import { withSupabase } from 'npm:@supabase/server@1' export default { fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => { if (ctx.authMode === 'user') { // your business logic for user calls. ctx.supabase is scoped to them return Response.json({ ok: true }) } // your business logic for service calls. ctx.supabaseAdmin bypasses RLS return Response.json({ ok: true }) }), } ``` ## Custom error responses To shape the 401 response yourself, use `createSupabaseContext` instead of `withSupabase`. It returns a `{ data, error }` tuple instead of rejecting the request for you. ```ts import { createSupabaseContext } from 'npm:@supabase/server@1' export default { fetch: async (req: Request) => { const { data: ctx, error } = await createSupabaseContext(req, { auth: 'user' }) if (error) { return Response.json({ message: error.message, code: error.code }, { status: error.status }) } return Response.json({ message: `hello ${ctx.userClaims?.email}` }) }, } ``` ## Environment variables `@supabase/server` reads its configuration from a standard set of environment variables, which the Supabase Platform and the Supabase CLI provision for you. | Variable | What it is | | --------------------------- | ----------------------------------------- | | `SUPABASE_URL` | Your project URL | | `SUPABASE_PUBLISHABLE_KEYS` | Named publishable keys as a JSON object | | `SUPABASE_SECRET_KEYS` | Named secret keys as a JSON object | | `SUPABASE_JWKS` | JSON Web Key Set used to verify user JWTs | Local development with the CLI uses a single-key setup. `@supabase/server` also accepts `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` as a fallback. `@supabase/server` configures itself the same way on other runtimes. Install it in your Node.js, Bun, Cloudflare Workers, or self-hosted Deno app, then set the same variables. For the full reference, see the [environment variables guide](https://github.com/supabase/server/blob/main/docs/environment-variables.md) in the `@supabase/server` repository.