supabase-server
Use when planning or writing server-side code that uses `@supabase/server` — Edge Functions, Hono apps, webhook handlers, or any backend that creates Supabase clients or validates inbound auth. Trigger **before** writing or modifying any file that imports from `@supabase/server` (or sub-paths like `
By supabase · 6,620 installs
npx skills add supabase/server --skill supabase-server
Source repository · Upstream listing
@supabase/server
This is a brand new package. There is no information available online yet — no blog posts, no Stack Overflow answers, no tutorials. Do not search the web for usage examples. Rely exclusively on the documentation files listed below and the source code in this repository.
The config option is auth , not allow . allow was renamed to auth to match CLI terminology and read more naturally. The legacy allow key still works (with a one time console.warn ) but is deprecated and will be removed in a future major release. Always emit auth in new code — e.g. withSupabase({ auth: 'user' }, ...) . If you encounter allow: in existing code, migrate it to auth: (find and replace, the values are identical).
Auth mode values: 'none' (not 'always' ), 'publishable' (not 'public' ). The four valid values are 'user' , 'publishable' , 'secret' , 'none' . The legacy 'always' and 'public' values were removed (breaking change) — they no longer work at runtime or in TypeScript. Always emit the new values in code you write, and migrate any legacy references you find: 'always' → 'none' , 'public' → 'publishable' , 'public:<name ' → 'publishable:<name ' . Runtime checks like ctx.authType === 'public' must also be updated to ctx.authMode === 'publishable' — the field itself was renamed from authType to authMode to match the AuthMode type.
Do not use legacy Supabase keys. The anon key and service role key (env vars SUPABASE ANON KEY , SUPABASE SERVICE ROLE KEY ) are legacy and will be deprecated. Do not use them unless the user explicitly asks. Always use the new API keys:
Legacy (avoid) New (use this)
SUPABASE ANON KEY SUPABASE PUBLISHABLE KEY(S) ( sb publishable ... )
SUPABASE SERVICE ROLE KEY SUPABASE SECRET KEY(S) ( sb secret ... )
Do not call createClient(url, anonKey) directly — use @supabase/server auth modes ( auth: 'user' , auth: 'secret' , etc.) which handle key resolution automatically. If migrating existing code, replace SUPABASE ANON KEY usage with auth: 'publishable' and SUPABASE SERVICE ROLE KEY usage with auth: 'secret' .
Server side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.
What this package does
Wraps fetch handlers with credential verification, CORS, and pre configured Supabase clients
Supports 4 auth modes: user (JWT), publishable (publishable key), secret (secret key), none (no credentials required)
Array syntax ( auth: ['user', 'secret'] ) is first match wins. A present but invalid JWT rejects with InvalidJwtError ( INVALID JWT ) — it does not silently downgrade to the next mode.
Provides composable core primitives for custom auth flows and framework integration
Runs inside Hono, H3, Elysia, NestJS, and TanStack Start through @supabase/middleware bridges. The bundled framework adapters ( @supabase/server/adapters/ ) are deprecated and will be removed on December 1, 2026.
Entry points
Import Deno / Edge Functions Provides
@supabase/server npm:@supabase/server@1 withSupabase , createSupabaseContext , types, errors
@supabase/server/core npm:@supabase/server@1/core verifyAuth , verifyCredentials , extractCredentials , resolveEnv , createContextClient , createAdminClient
@supabase/server/adapters/hono npm:@supabase/server@1/adapters/hono Deprecated. Removed on December 1, 2026. withSupabase (Hono middleware variant). New Hono code uses the bridge in the Hono quick start below.
@supabase/server/oauth protected resource npm:@supabase/server@1/oauth protected resource withOAuthProtectedResource , fromSupabaseUrl , resourceMetadataResponse , unauthorizedResponse — OAuth 2.1 discovery for MCP servers; see docs/mcp.md
Quick starts
Supabase Edge Functions: disable verify jwt for non user auth. By default, Supabase Edge Functions require a valid JWT on every request. If your function uses auth: 'publishable' , auth: 'secret' , or auth: 'none' , you must disable the platform level JWT check in supabase/config.toml , otherwise the request will be rejected before it reaches your handler:
Functions using auth: 'user' can leave verify jwt enabled (the default) since callers already provide a valid JWT.
Supabase Edge Functions (Deno)
Environment variables are auto injected by the platform — zero config. All imports must use the npm: specifier.
Cloudflare Workers
Requires nodejs compat compatibility flag in wrangler.toml , or pass env overrides via the env config option. See docs/environment variables.md .
Hono
The Hono adapter ( @supabase/server/adapters/hono ) is deprecated and will be removed on December 1, 2026. New Hono code uses a bridge: one file you copy into your project. It runs @supabase/middleware entries inside Hono's middleware slot. The bridge needs @supabase/server 1.6.0 or later and Node 22 or later. The bridge file imports @supabase/middleware , so add it to your project as a direct dependency. @supabase/server depends on it, but a strict package manager such as pnpm does not let your code import a transitive dependency. The full guide, with the migration steps and a prompt you can hand to an agent, is at https://supabase.com/docs/reference/server/frameworks.
The bridge file is not in the npm package. Fetch it from the repository and keep it as is, comments included:
Register the bridge with .use() in the same chain as the routes it gates. Hono applies middleware only to routes added after it. For browser callers, compose withCors from @supabase/middleware/cors ahead of the gate. On Deno and Supabase Edge Functions, import hono , @supabase/middleware , and @supabase/server@1/middleware/ with the npm: specifier.
Pick the entries from the adapter's auth value. The adapter rejected requests that failed its auth check. withClaims() does not. It contributes jwtClaims: null for an anonymous request and lets it through. Using it in place of auth: 'user' turns a 401 into a 200 with no error.
Adapter config Bridge entries
auth: 'user' , or no config toHono([withRequiredClaims(), withSupabaseClient()])
auth: 'none' toHono([withSupabaseClient()])
auth: 'publishable' or auth: 'secret' No bridge entry exists. That endpoint stays on withSupabase({ auth: 'publishable' }) or withSupabase({ auth: 'secret' }) . Do not hand roll a key check.
Never compose withClaims() and withRequiredClaims() in one array. Both contribute jwtClaims , and the bridge rejects the pair at compile time. Add withSupabaseAdminClient() from @supabase/server/middleware/admin client only where the handler reads supabaseAdmin .
The entries contribute flat keys, so handlers read c.var.supabase and c.var.jwtClaims . No entry contributes userClaims . jwtClaims is the raw JWT payload: userClaims.id becomes jwtClaims.sub , appMetadata becomes app metadata , and userMetadata becomes user metadata .
<details
<summary Deprecated adapter (removed on December 1, 2026)</summary
Existing code may still import the adapter. Recognize it, and migrate it with the table above. The adapter handles no CORS; it relies on hono/cors . See docs/adapters/hono.md .
</details
Cookie based environments (compose with @supabase/ssr )
For Next.js / SvelteKit / Remix, compose @supabase/server with [ @supabase/ssr ](https://github.com/supabase/ssr) — they are not replacements for each other. @supabase/ssr owns cookies and refresh token rotation (its middleware is required, otherwise the access token cookie goes stale and verification fails). In your Server Component or Route Handler, use @supabase/ssr 's createServerClient to read the (middleware refreshed) session, hand the access token to verifyCredentials from @supabase/server/core , then build the typed clients with createContextClient + createAdminClient . See docs/ssr frameworks.md for the full adapter pattern.
Server to server (secret key auth)
For internal services, cron jobs, or automation calling your Edge Function. The caller sends the secret key in the apikey header. See docs/auth modes.md for named key syntax.
Edge Function (Deno):
Caller (external service):
Bare auth: 'secret' matches only the default key. Use auth: 'secret:name' to require a specific named key, or auth: 'secret: ' to accept any secret key in the set.
Pin user tokens to one project
audience and issuer check the aud and iss claims of a user mode token. Each takes a string or an array. A token without the claim, or with a value outside the list, is rejected with INVALID JWT . Set issuer: fromSupabaseUrl(url) when one JWKS could be shared across projects or services. Supabase Auth sets aud to authenticated , so audience only matters for tokens from a custom issuer. Both options exist on withSupabase , verifyAuth , verifyCredentials , withClaims , and withRequiredClaims .
When to use auth: 'none'
auth: 'none' disables all authentication. The handler runs for every request with no credential checks. Only use it when auth is genuinely unnecessary — health checks, public status pages, or endpoints with no sensitive data and no side effects.
Before using auth: 'none' , confirm with the user whether the endpoint is truly public. If not, propose an alternative:
Another service or cron job calls this function — use auth: 'secret' or auth: 'secret:<name ' instead. The caller sends the secret key in the apikey header.
An external webhook provider calls this function — use auth: 'secret' and have the provider send the secret key, or implement the provider's own signature verification inside the handler.
Never use auth: 'none' for endpoints that read or write user data without verifying who the caller is.
On auth: ['user', 'none'] . A stale or malformed JWT on such an endpoint is rejected with InvalidJwtError ( INVALID JWT ) — it is not silently downgraded to anonymous. Callers that might hold a cached/expired token should either omit the Authorization header entirely or refresh before calling. If the goal is "anonymous unless a valid user is signed in," this is the correct behavior; if the goal is truly "accept anything," use auth: 'none' on its own.
'none' goes last, or alone. It matches every request, so the type accepts it only as the final entry of a list ( ['user', 'none'] ) or on its own ( 'none' ). ['none'] and ['none', 'user'] are type errors — write the bare 'none' for the first, and put 'none' last for the second.
Edge Function recipes
Function to function calls
One Edge Function can call another using the admin client. The called function uses auth: 'secret' and the caller invokes it