withSupabase middleware for Edge Functions
The `withSupabase` middleware from `@supabase/server` wraps an Edge Function to provide authentication and database context. It accepts an options object with an `auth` field. Use `auth: 'publishable'` to accept requests with a publishable API key in the `apikey` header. The middleware passes a `ctx` object with `ctx.supabase` for storage and database access.
verify_jwt does not accept API keys
The verify_jwt platform check does not accept API keys. Publishable and secret keys are not JWTs, so callers that send an API key in the Authorization header fail the platform check before their request reaches the handler.
When to leave verify_jwt enabled
Leave verify_jwt enabled (the default) for functions that are only called with a user JWT, such as functions invoked from the client through supabase.functions.invoke. The platform rejects unauthenticated requests before they reach the code, and the handler can trust that a valid JWT is present.
When to turn verify_jwt off
Turn verify_jwt off for functions that are called without an Authorization header, such as webhooks from external providers or service-to-service calls that authenticate with an API key.
verify_jwt platform check runs before handler code
Every request to an Edge Function passes through a platform-level verify_jwt check before the handler code executes. When verify_jwt is enabled (the default), the platform inspects the Authorization header and expects a valid user JWT. If the header is missing, malformed, or signed with a different key, the platform returns a 401 error and the code never executes. The check validates both legacy HS256 JWTs and JWTs signed with new asymmetric signing keys.
Two authorization headers for Edge Functions
Edge Functions recognize two request headers: Authorization (for user JWTs signed in through Supabase Auth, format 'Bearer <user-jwt>') and apikey (for API keys from clients or services, format 'sb_publishable_...' or 'sb_secret_...'). A common mistake is sending a publishable or secret key as a bearer token in the Authorization header, which fails validation since API keys are not JWTs. Instead, put API keys in the apikey header. Both headers can be sent together; for example, a signed-in user calling through supabase-js sends their session JWT in Authorization and the project's publishable key in apikey.
Example: Creating Supabase client with auth context in Edge Functions
```js
import { createClient } from 'npm:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
})
```
This example shows how to create a Supabase client with the auth context of the user that called the function, so that Row Level Security policies are applied.
Set up auth context in Edge Functions using Authorization header
When a user makes a request to an Edge Function, you can use the Authorization header to set the Auth context in the Supabase client and enforce Row Level Security policies. Create the Supabase client with a global headers configuration that passes the Authorization header from the request. This context setting happens in the Deno.serve() callback argument so that the Authorization header is set for each individual request scope.
Example: Fetching user from authorization header
```js
Deno.serve(async (req: Request) => {
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const { data } = await supabaseClient.auth.getUser(token)
})
```
This example shows how to extract the JWT token from the Authorization header and fetch the user object.
Row Level Security is enforced in Edge Functions with auth context
After initializing a Supabase client with the Auth context, all queries will be executed with the context of the user. For database queries, this means Row Level Security policies will be enforced, and users will only see rows they have access to.
Example: Database query with RLS in Edge Functions
```js
import { createClient } from 'npm:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
// This query respects RLS - users only see rows they have access to
const { data, error } = await supabaseClient.from('profiles').select('*');
if (error) {
return new Response('Database error', { status: 500 })
}
})
```
This example shows how database queries automatically respect Row Level Security policies when the Supabase client is initialized with auth context.
Edge Functions can identify users through Legacy JWT tokens
Supabase Edge Functions work with Supabase Auth to automatically identify users through Legacy JWT tokens passed in the Authorization header.
Fetch user from JWT token in Edge Functions
You can get the JWT from the Authorization header and provide it to getUser() to fetch the user object and obtain metadata for the logged in user. Extract the token by removing the 'Bearer ' prefix from the Authorization header, then call supabaseClient.auth.getUser(token).
withSupabase example with RLS and error handling
Example showing withSupabase usage with auth: 'user' option. The ctx.supabase respects the caller's RLS policies while ctx.supabaseAdmin bypasses RLS for privileged operations. Responses are automatically formatted as JSON with built-in error handling:
```ts
import { withSupabase } from 'npm:@supabase/server@^1'
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
try {
const { data, error } = await ctx.supabase.from('countries').select('*')
if (error) {
throw error
}
return Response.json({ data })
} catch (err) {
return Response.json({ error: String(err?.message ?? err) }, { status: 500 })
}
}),
}
```
withSupabase enables RLS enforcement, JSON serialization, and TypeScript support
The withSupabase wrapper provides automatic Row Level Security enforcement, built-in JSON serialization, consistent error handling, and TypeScript support for database schema.
withSupabase wrapper provides scoped client and admin access
The withSupabase wrapper from @supabase/server provides ctx.supabase, a supabase-js client automatically scoped to the caller's Row Level Security policies without requiring manual key or authorization header management. It also provides ctx.supabaseAdmin for privileged operations that bypass Row Level Security. Responses are automatically formatted as JSON. This is the recommended approach for most applications.
Custom error responses example with createSupabaseContext
import { createSupabaseContext } from 'npm:@supabase/server'
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}` })
},
}
This example shows how to use createSupabaseContext to handle authentication errors with custom responses.
Security caution for auth: 'none'
auth: 'none' disables every credential check. Never use it on an endpoint that reads or writes sensitive data without verifying the caller some other way. The handler is fully responsible for authenticating the caller.
withSupabase wrapper validates credentials against auth mode
The withSupabase wrapper from @supabase/server verifies the caller's credentials against a declared auth mode and provides a pre-configured Supabase client on ctx.
Auth modes for Edge Functions
Edge Functions support four auth modes: 'user' accepts a valid user JWT on the Authorization header; 'secret' accepts a secret key on the apikey header; 'publishable' accepts a publishable key on the apikey header; 'none' accepts any caller with no credential check.
User JWT authentication with auth: 'user'
When auth is set to 'user', the function receives a user JWT on the Authorization header. Keep verify_jwt = true (the default) so the platform validates the JWT before the handler runs. This mode provides ctx.supabase already scoped to the caller's RLS policies.
ctx properties with withSupabase
The withSupabase wrapper provides ctx with the following properties: supabase (RLS-scoped to the authenticated user), supabaseAdmin (bypasses RLS using service role), userClaims (user identity from JWT including id, email, role), jwtClaims (full JWT claims), and authMode (which auth mode matched).
Service-to-service calls with auth: 'secret'
For service-to-service calls from cron jobs, workers, pg_net, or other Edge Functions, disable verify_jwt and use auth: 'secret' to validate the key against any secret key from the dashboard. This mode provides ctx.supabaseAdmin for privileged work.
Specific secret key authentication with auth: 'secret:<name>'
To accept only one specific secret key, use auth: 'secret:<name>' where name is the key name from Settings > API keys in the Dashboard. For example, auth: 'secret:automations' only accepts the secret key named 'automations'. The same syntax works for publishable keys with auth: 'publishable:<name>'.
Public functions with auth: 'none'
For genuinely public functions like health checks, use auth: 'none' with verify_jwt = false so anonymous callers can reach the handler.
External webhook authentication pattern
External providers like Stripe or GitHub sign requests with their own shared secret, not Supabase credentials. Use auth: 'none' to skip the SDK's credential check, verify the provider's signature inside the handler using their library, and keep verify_jwt = false.
Combining multiple auth modes
Functions can accept multiple auth modes as an array on the auth parameter, such as auth: ['user', 'secret']. Modes are tried in order and the first match wins. Use ctx.authMode to determine which mode matched.
Custom error responses with createSupabaseContext
Use createSupabaseContext instead of withSupabase to have control over error responses. It returns a { data, error } tuple. If error is present, you can shape the response yourself with custom status codes and messages.
Authenticated user calls example
import { withSupabase } from 'npm:@supabase/server'
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 })
}),
}
This example shows how to handle authenticated user calls by setting auth to 'user'.
Service-to-service calls example
import { withSupabase } from 'npm:@supabase/server'
export default {
fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => {
// your business logic. ctx.supabaseAdmin bypasses RLS
return Response.json({ ok: true })
}),
}
This example shows how to handle service-to-service calls with a secret key.
Public function example
import { withSupabase } from 'npm:@supabase/server'
export default {
fetch: withSupabase({ auth: 'none' }, async () => {
// your business logic
return Response.json({ ok: true })
}),
}
This example shows a public function with auth: 'none' and verify_jwt = false in the TOML configuration.
External webhook example with Stripe
import { withSupabase } from 'npm:@supabase/server'
import Stripe from 'npm:stripe'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)
export default {
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
const signature = req.headers.get('stripe-signature') ?? ''
const body = await req.text()
try {
stripe.webhooks.constructEvent(body, signature, Deno.env.get('STRIPE_WEBHOOK_SECRET')!)
} catch {
return new Response('bad signature', { status: 400 })
}
// your business logic. ctx.supabaseAdmin available for db work
return Response.json({ received: true })
}),
}
This example shows how to handle Stripe webhooks by verifying the signature and using auth: 'none'.
Combining auth modes example
import { withSupabase } from 'npm:@supabase/server'
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 })
}),
}
This example shows how to handle both user and service-to-service calls by combining auth modes.
Send custom emails from auth hooks with React Email and Resend
Edge Functions can be configured to handle custom auth emails using the send email hook. The function receives a webhook payload containing user email, token, token_hash, redirect_to, and email_action_type. This data can be used with React Email components to render HTML and send emails via Resend.
Send Email Hook payload structure
The send email hook webhook payload contains: user object with email property, and email_data object with token (string), token_hash (string), redirect_to (string), email_action_type (string), site_url (string), token_new (string), and token_hash_new (string).
Verify webhook signatures with standardwebhooks
To verify webhook authenticity in Edge Functions, use the Webhook class from the standardwebhooks package. Initialize it with the hook secret (with 'v1,whsec_' prefix removed), then call wh.verify(payload, headers) to validate the webhook and extract its payload.
Configure send email hook in Supabase dashboard
Send email hooks are configured in the Auth Hooks section of the Supabase dashboard. Create a new Send Email hook, select HTTPS as the hook type, paste the function URL, generate a webhook secret, and save the configuration. The webhook secret is in the format 'v1,whsec_<base64_secret>'.
withSupabase handler wrapper with auth disabled
Use withSupabase({ auth: 'none' }, async (req, ctx) => {...}) to create an open endpoint. This should be used for testing only; in production implement an authorization layer in the handler or switch the auth mode.
withSupabase middleware with auth option
The withSupabase middleware from @supabase/server accepts a configuration object with an auth property that can be set to 'none' to disable JWT verification for public endpoints.
Use withSupabase for auth middleware
The `withSupabase` middleware from `npm:@supabase/server@^1` provides automatic Supabase client initialization. It accepts an options object with an `auth` property (can be 'none' to disable JWT verification) and a handler function that receives the request and a context object containing `supabaseAdmin` for admin-level database access.
Access admin Supabase client from context
When using `withSupabase` middleware, the context object passed to the handler function contains `ctx.supabaseAdmin`, which is a Supabase client with admin privileges for performing database operations.
Rate limit based on user ID from Supabase Auth
Rate limiting in Supabase Edge Functions can be implemented based on the user ID extracted from Supabase Auth, allowing per-user rate limit enforcement.
MCP server security best practices
When deploying MCP servers: do not expose sensitive data or use non-production data; implement proper authentication for production deployments; always validate and sanitize tool inputs; only expose necessary tools for your use case; track tool calls and monitor for unusual activity.
Authentication in MCP servers uses --no-verify-jwt flag
The template uses --no-verify-jwt for quick development, which means Supabase's JWT layer does not enforce authentication. For production, implement authentication at the MCP server level following the MCP Authorization specification to control who can access MCP tools.
withSupabase helper with secret authentication
The withSupabase helper from npm:@supabase/server@^1 can be configured with auth: 'secret' to accept webhook authentication using a secret key instead of JWT verification. The function parameter receives a req object for the webhook payload and a ctx object with ctx.supabaseAdmin for database queries.
withSupabase wrapper for authentication
Import and use `withSupabase` from 'npm:@supabase/server' to wrap handler functions. It accepts an auth configuration array specifying which auth methods are allowed, such as `withSupabase({ auth: ['user', 'secret'] }, handler)` to allow both signed-in users (via JWT) and secret keys.
withSupabase middleware usage
Use `withSupabase({ auth: 'none' }, async (req, ctx) => {...})` as middleware to wrap Edge Function handlers. The auth option can be set to 'none' to disable authentication for open endpoints.
WebSocket upgrade and JWT validation in Edge Functions
Use Deno.upgradeWebSocket(req, { idleTimeout: 0 }) to upgrade HTTP requests to WebSocket connections. Extract the JWT token from query parameters (url.searchParams.get('token')). Validate the token using verifyCredentials({ token, apikey: null }, { auth: 'user' }). Extract the user ID from auth.userClaims.id. Run the function with `supabase functions serve --no-verify-jwt` to validate JWT inside the function rather than at the platform level.
withSupabase wrapper with auth: 'user'
When deploying an edge function that authenticates users, use withSupabase({ auth: 'user' }) wrapper. This verifies JWT and provides both ctx.supabase (user client) and ctx.supabaseAdmin (admin client) in the handler.
withSupabase wrapper with auth: 'secret'
When deploying an edge function as a database webhook that uses a secret key for authentication, use withSupabase({ auth: 'secret' }) wrapper. Deploy the function with verify_jwt = false.
Slack bot withSupabase middleware configuration
When building a Slack bot Edge function, use withSupabase({ auth: 'none' }, ...) to disable authentication verification since Slack itself sends requests to the function endpoint, not an authenticated user.
verify_jwt configuration for disabling authentication
Set verify_jwt = false in the [functions.function-name] section of config.toml to disable JWT authentication for a specific function. This is commonly used for webhook functions like Stripe webhooks that need to be publicly accessible.
Security warning for disabling JWT verification
Disabling JWT verification with verify_jwt = false or --no-verify-jwt flag allows anyone to invoke your Edge Function without a valid JWT token. This should be used carefully and only for functions that need to be publicly accessible, such as webhook handlers.
withSupabase helper with auth none for open endpoints
The `withSupabase({ auth: 'none' }, handler)` wrapper allows creating open endpoints without JWT verification. The comment notes this is for testing only and production should implement an authorization layer in the handler or switch the auth mode.
withSupabase wrapper for Edge Functions with Kysely
Use the withSupabase({ auth: 'user' }) wrapper to require user authentication in Edge Functions. This provides context (ctx) alongside the request object. Queries are executed within the try-catch block, and results must be serialized (converting BigInt values to strings) before returning as JSON.
API keys for invoking functions
To invoke an Edge Function from your application, you need API keys found in Settings > API Keys. Publishable Keys are for client-side requests (safe in browsers with RLS enabled). Secret Keys are for server-side requests (should be kept secret; bypasses RLS).
Default function template with auth handling
The default starter code created by 'supabase functions new' includes 'export default { fetch: withSupabase({ auth: ["publishable", "secret"] }, async (req, ctx) => { const { name } = await req.json() return Response.json({ message: `Hello ${name}!` }) }) }'. This basic template accepts a JSON payload with a 'name' field and returns a greeting message. The auth parameter can be changed with the --auth flag when creating a new function.
Use withSupabase for clean authenticated database access in functions
The withSupabase helper from @supabase/server automatically provides an authenticated ctx.supabase client to Edge Functions. This pattern simplifies authenticated database access and integrates well with mocking strategies for testing.
Generate realistic JWTs in tests for authenticated functions
When testing Edge Functions that rely on authenticated Supabase clients, generate valid RS256 JWTs in test setup. This allows testing the real authentication flow without requiring a running Supabase instance.
Authenticate WebSocket clients with JWT in query params
To authenticate WebSocket clients when standard header-based JWT authentication is unavailable, pass the JWT as a URL query parameter. Extract it using new URL(req.url).searchParams.get('jwt'), then validate it with supabase.auth.getUser(jwt). Be aware that query params may be logged in logging systems.