Use withSupabase or createSupabaseContext for standard use cases. Drop down to core primitives when you need:
- Multiple routes with different auth in a single handler
- Custom response headers or error formats
- Integration with frameworks other than the ones provided
- Pre-extracted credentials (e.g., from cookies, custom headers)
- Just auth verification without client creation
All primitives are available from @supabase/server/core.
The primitives compose into a pipeline. Each step is independent — use only what you need:
resolveEnv() → SupabaseEnv
extractCredentials(request) → Credentials { token, apikey }
verifyCredentials(credentials, opts) → AuthResult { authMode, token, userClaims, jwtClaims, keyName }
createContextClient(options) → SupabaseClient (RLS-scoped)
createAdminClient(options) → SupabaseClient (bypasses RLS)
Or use the convenience function that combines extraction and verification:
verifyAuth(request, opts) → AuthResult (extractCredentials + verifyCredentials in one call)
Resolves Supabase environment configuration from runtime variables. The only hard requirement is SUPABASE_URL.
import { resolveEnv } from '@supabase/server/core'
const { data: env, error } = resolveEnv()
if (error) {
// error is an EnvError — e.g., SUPABASE_URL not set
console.error(error.message)
}With partial overrides:
const { data: envOverridden } = resolveEnv({
url: 'http://localhost:54321',
})Returns { data: SupabaseEnv, error: null } on success, { data: null, error: EnvError } on failure.
Pure extraction — reads headers, performs no validation.
import { extractCredentials } from '@supabase/server/core'
const creds = extractCredentials(request)
// creds.token → string | null (from Authorization: Bearer <token>)
// creds.apikey → string | null (from apikey header)This is synchronous and never fails. Fields are null when the corresponding header is absent.
Verifies pre-extracted credentials against allowed auth modes. Use this when credentials come from a non-standard source (cookies, custom headers, etc.).
import { verifyCredentials } from '@supabase/server/core'
const credentials = { token: cookieToken, apikey: null }
const { data: auth, error } = await verifyCredentials(credentials, {
auth: 'user',
})
if (error) {
return Response.json({ message: error.message }, { status: error.status })
}
console.log(auth!.authMode) // 'user'
console.log(auth!.userClaims) // { id: '...', email: '...', role: 'authenticated' }Supports all auth mode syntax — single mode, arrays, and named keys:
// Multiple modes
const { data: auth } = await verifyCredentials(creds, {
auth: ['user', 'publishable'],
})
// Named key
const { data: auth } = await verifyCredentials(creds, {
auth: 'publishable:web',
})
// Wildcard
const { data: auth } = await verifyCredentials(creds, {
auth: 'secret:*',
})Convenience function that combines extractCredentials and verifyCredentials in a single call. Use this when working with a standard Request:
import { verifyAuth } from '@supabase/server/core'
const { data: auth, error } = await verifyAuth(request, {
auth: 'user',
})
if (error) {
return Response.json({ message: error.message }, { status: error.status })
}
console.log(auth.userClaims!.id) // "d0f1a2b3-..."
console.log(auth.token) // the verified JWT stringCreates a Supabase client scoped to the caller's identity. RLS policies apply.
import { verifyAuth, createContextClient } from '@supabase/server/core'
// With a user's token (from verifyAuth)
const { data: auth } = await verifyAuth(request, { auth: 'user' })
const supabase = createContextClient({
auth: { token: auth!.token, keyName: auth!.keyName },
})// Anonymous (no token) — RLS as anon role
const anonClient = createContextClient()The client is configured with:
- The publishable key as the
apikeyheader - The user's JWT as the
Authorization: Bearerheader (if token is provided) - Server-safe auth settings:
persistSession: false,autoRefreshToken: false,detectSessionInUrl: false
Which key is used. With auth.keyName set, that named key from SUPABASE_PUBLISHABLE_KEYS is used (and it throws if the key doesn't exist). With keyName omitted, the default key is used, falling back to the first key in the set when no default exists. Note this differs from the 'publishable' auth mode, which matches the default key only and never falls back.
This function throws EnvError if SUPABASE_URL or the required publishable key is missing. Wrap in try/catch when using directly.
Creates a Supabase client that bypasses Row-Level Security using a secret key.
import { createAdminClient } from '@supabase/server/core'
const supabaseAdmin = createAdminClient()// With a specific named key
const supabaseAdminInternal = createAdminClient({
auth: { keyName: 'internal' },
})Which key is used. With auth.keyName set, that named key from SUPABASE_SECRET_KEYS is used (and it throws if the key doesn't exist). With keyName omitted, the default key is used, falling back to the first key in the set when no default exists. Note this differs from the 'secret' auth mode, which matches the default key only and never falls back.
Same server-safe settings as createContextClient. Throws EnvError if the secret key is missing.
Using primitives to build a handler with different auth per route, without a framework:
import {
verifyAuth,
createContextClient,
createAdminClient,
} from '@supabase/server/core'
export default {
fetch: async (req: Request) => {
const url = new URL(req.url)
// Public route — no auth needed
if (url.pathname === '/health') {
return Response.json({ status: 'ok' })
}
// User-authenticated route
if (url.pathname === '/todos') {
const { data: auth, error } = await verifyAuth(req, { auth: 'user' })
if (error) {
return Response.json(
{ message: error.message },
{ status: error.status },
)
}
const supabase = createContextClient({
auth: { token: auth!.token, keyName: auth!.keyName },
})
const { data } = await supabase.from('todos').select()
return Response.json(data)
}
// Admin route — secret key only
if (url.pathname === '/admin/users') {
const { data: auth, error } = await verifyAuth(req, {
auth: 'secret',
})
if (error) {
return Response.json(
{ message: error.message },
{ status: error.status },
)
}
const supabaseAdmin = createAdminClient({
auth: { keyName: auth!.keyName },
})
const { data } = await supabaseAdmin.from('profiles').select()
return Response.json(data)
}
return new Response('Not found', { status: 404 })
},
}In Next.js, SvelteKit, Remix, and other cookie-based frameworks, the JWT lives in session cookies rather than the Authorization header. The recommended pattern is to compose with @supabase/ssr: let @supabase/ssr own the cookie session lifecycle and refresh-token rotation (via middleware), then hand its fresh access token to verifyCredentials and build typed clients with createContextClient + createAdminClient.
For the full pattern — middleware setup, the composed adapter, JWKS caching, and other-framework adapting tips — see ssr-frameworks.md.