Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
@supabase/server
Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
const { data } = await ctx.supabase.from('todos').select()
return Response.json(data)
}),
}
One import. One line of config. Auth is validated, clients are scoped, CORS is handled. Your handler only runs on successful auth.
Installation
# npm
npm install @supabase/server
# pnpm
pnpm add @supabase/server
# Deno / Supabase Edge Functions (no install — import directly)
import { withSupabase } from "npm:@supabase/server";
AI coding skills
Install the skill so your AI coding agent (Claude Code, Cursor, etc.) knows how to use this package:
npx skills add supabase/server
Quick Start
Authenticated endpoint
export default {
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
// ctx.supabase — RLS-scoped to the authenticated user
// ctx.supabaseAdmin — bypasses RLS (service role)
// ctx.userClaims — user identity from JWT (id, email, role)
// ctx.claims — JWT claims
// ctx.authType — which auth mode matched
const { data } = await ctx.supabase.from('todos').select()
return Response.json(data)
}),
}
Public endpoint (no auth)
export default {
fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
return Response.json({ status: 'ok' })
}),
}
API key protected
export default {
fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
const { data } = await ctx.supabaseAdmin.from('config').select()
return Response.json(data)
}),
}
Dual auth (user or service)
export default {
fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
const userId = ctx.userClaims?.id ?? (await req.json()).user_id
const { data } = await ctx.supabaseAdmin
.from('reports')
.select()
.eq('user_id', userId)
return Response.json(data)
}),
}
Server-to-server
// Only accept the "automations" named secret key
export default {
fetch: withSupabase({ allow: 'secret:automations' }, async (req, ctx) => {
const body = await req.json()
const { data } = await ctx.supabaseAdmin
.from('scheduled_tasks')
.insert({ name: body.taskName })
return Response.json({ success: true, data })
}),
}
The caller sends the secret key in the apikey header:
await fetch('https://<project>.supabase.co/functions/v1/my-function', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: 'sb_secret_...', // the "automations" secret key
},
body: JSON.stringify({ taskName: 'cleanup' }),
})
Auth Modes
| Mode | Credential | Use case |
|---|---|---|
"user" (default) |
Valid JWT | Authenticated user endpoints |
"public" |
Valid publishable key | Client-facing, key-validated endpoints |
"secret" |
Valid secret key | Server-to-server, internal calls |
"always" |
None | Open endpoints, wrappers that handle their own auth |
Array syntax (allow: ["user", "secret"]) accepts multiple auth methods — first match wins.
Named key validation: allow: "public:web_app" or allow: "secret:automations" validates against a specific named key in SUPABASE_PUBLISHABLE_KEYS or SUPABASE_SECRET_KEYS.
Supabase Edge Functions: By default, the platform requires a valid JWT on every request. If your function uses
allow: 'public',allow: 'secret', orallow: 'always', disable the platform-level JWT check insupabase/config.toml:[functions.my-function] verify_jwt = false
Context
Every handler receives a SupabaseContext:
interface SupabaseContext {
supabase: SupabaseClient // RLS-scoped (user or anon depending on auth)
supabaseAdmin: SupabaseClient // Bypasses RLS
userClaims: UserClaims | null // JWT-derived identity (for full User, call supabase.auth.getUser())
claims: JWTClaims | null // Present when auth is JWT
authType: Allow // Which auth mode matched
}
supabase is always the safe client — it respects RLS. When authType is "user", it's scoped to that user's permissions. Otherwise, it's initialized as anonymous.
supabaseAdmin always bypasses RLS. Use it for operations that need full database access.
Config
withSupabase(
{
allow: 'user', // who can call this function
cors: false, // disable CORS (default: supabase-js CORS headers)
env: { url: '...' }, // env overrides (optional)
},
handler,
)
cors defaults to the standard supabase-js CORS headers. Pass a Record<string, string> to set custom headers, or false to disable CORS handling (e.g. when using a framework that handles CORS separately).
withSupabase(
{
allow: 'user',
cors: {
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Headers': 'authorization, content-type',
},
},
handler,
)
env overrides environment variable resolution. Defaults to reading SUPABASE_URL, SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS, and SUPABASE_JWKS from the runtime environment.
Framework Adapters
Hono
import { Hono } from 'hono'
import { withSupabase } from '@supabase/server/adapters/hono'
const app = new Hono()
app.get('/todos', withSupabase({ allow: 'user' }), async (c) => {
const { supabase: sb } = c.var.supabaseContext
const { data } = await sb.from('todos').select()
return c.json(data)
})
app.get('/health', (c) => c.json({ status: 'ok' }))
export default { fetch: app.fetch }
The adapter does not handle CORS — use hono/cors for that. Per-route auth works naturally by applying the middleware to specific routes.
Primitives
For when you need more control than withSupabase provides — multiple routes with different auth, custom response headers, or building your own wrapper.
All primitives are available from @supabase/server/core.
import {
verifyAuth,
createContextClient,
createAdminClient,
} from '@supabase/server/core'
verifyAuth
Extracts credentials from a Request and validates against the allow config.
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
if (error) {
return Response.json({ message: error.message }, { status: error.status })
}
verifyCredentials
Low-level — works with raw credentials instead of a Request. Used by SSR adapters and custom auth flows.
const credentials = { token: myToken, apikey: null }
const { data: auth, error } = await verifyCredentials(credentials, {
allow: 'user',
})
createContextClient / createAdminClient
const supabase = createContextClient(auth.token) // user-scoped, RLS applies
const supabase = createContextClient() // anonymous, RLS as anon
const supabaseAdmin = createAdminClient() // bypasses RLS
createSupabaseContext
Full context assembly from a Request — verifyAuth + client creation in one call.
const { data: ctx, error } = await createSupabaseContext(req, { allow: 'user' })
resolveEnv
Resolves environment variables with optional overrides.
const { data: env, error } = resolveEnv({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
})
Example: custom multi-route handler
import { verifyAuth, createContextClient } from '@supabase/server/core'
export default {
fetch: async (req) => {
const url = new URL(req.url)
if (url.pathname === '/health') {
return Response.json({ status: 'ok' })
}
if (url.pathname === '/todos') {
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
if (error)
return Response.json(
{ message: error.message },
{ status: error.status },
)
const supabase = createContextClient(auth.token)
const { data } = await supabase.from('todos').select()
return Response.json(data)
}
return new Response('Not found', { status: 404 })
},
}
Environment Variables
Automatically available in Supabase Edge Functions:
| Variable | Format | Description |
|---|---|---|
SUPABASE_URL |
https://<ref>.supabase.co |
Your project URL |
SUPABASE_PUBLISHABLE_KEYS |
{"default":"sb_publishable_...","web":"sb_publishable_..."} |
Publishable API keys (named) |
SUPABASE_SECRET_KEYS |
{"default":"sb_secret_...","web":"sb_secret_..."} |
Secret API keys (named) |
SUPABASE_JWKS |
{"keys":[...]} or [...] |
JSON Web Key Set for JWT verification |
Also supported (for local dev, self-hosted, or other runtimes):
| Variable | Format | Description |
|---|---|---|
SUPABASE_PUBLISHABLE_KEY |
sb_publishable_... |
Single publishable key |
SUPABASE_SECRET_KEY |
sb_secret_... |
Single secret key |
When both singular and plural forms are set, plural takes priority.
For other environments, pass overrides via the env config option or resolveEnv(). See docs/environment-variables.md for details.
Runtimes
- Supabase Edge Functions — environment variables are auto-injected. Zero config.
- Deno / Bun — works out of the box with the
export default { fetch }pattern. - Node.js — use the Hono adapter or core primitives with your framework of choice.
- Cloudflare Workers — enable
nodejs_compatinwrangler.tomlor pass env overrides via theenvconfig option. - Next.js / Nuxt / SvelteKit / Remix — use core primitives to build a cookie-based auth adapter. See
docs/ssr-frameworks.md.
Exports
| Export | What's in it |
|---|---|
@supabase/server |
withSupabase, createSupabaseContext |
@supabase/server/core |
verifyAuth, verifyCredentials, extractCredentials, createContextClient, createAdminClient, resolveEnv |
@supabase/server/adapters/hono |
withSupabase (Hono middleware) |
Documentation
| Question | Doc file |
|---|---|
| How do I create a basic endpoint? | docs/getting-started.md |
| What auth modes are available? Array syntax? Named keys? | docs/auth-modes.md |
| How do I use this with Hono? | docs/hono-adapter.md |
| How do I use low-level primitives for custom flows? | docs/core-primitives.md |
| How do environment variables work across runtimes? | docs/environment-variables.md |
| How do I handle errors? What codes exist? | docs/error-handling.md |
| How do I get typed database queries? | docs/typescript-generics.md |
| How do I use this in Next.js, Nuxt, SvelteKit, or Remix? | docs/ssr-frameworks.md |
| What's the complete API surface? | docs/api-reference.md |
Development
pnpm install
pnpm dev
Contributing
See CONTRIBUTING.md for development workflow, commit conventions, and release process.
License
MIT