* feat: specific, self-identifying errors with hints and diagnostics
Nearly every failure returned `{ message: "Invalid credentials", code:
"INVALID_CREDENTIALS" }` — naming neither the cause nor the library it
came from.
Provenance. All errors now share a `SupabaseServerError` base carrying
`source: "@supabase/server"`, a `[@supabase/server]` message prefix (the
convention `deprecation.ts` already used for warnings), a `docs` link to
the matching `docs/error-handling.md` section, an optional `hint`, and
non-sensitive `details`. `toJSON()` renders the wire payload and is picked
up by `JSON.stringify`, so logging no longer yields `{}`. One
`errorResponse()` helper renders it everywhere, repeating the code in an
`x-supabase-server-error` header and adding that to
`Access-Control-Expose-Headers` so cross-origin callers can read it.
Top-level `message` and `code` are unchanged, so existing consumers and
the adapters keep working.
Diagnosis. `verifyUserJwt` now returns *why* a token failed instead of
`null`, and the mode chain records why each mode fell through, so the
final error names the real cause: `MISSING_CREDENTIALS`,
`INVALID_API_KEY`, `INVALID_JWT`, plus `JWKS_NOT_CONFIGURED`,
`JWKS_FETCH_FAILED` and `NO_KEYS_CONFIGURED` for states where no request
could ever have succeeded. `INVALID_CREDENTIALS` stays exported as the
fallback. Hints cover the mistakes people actually make — a secret key
sent to a publishable-only endpoint, a legacy anon/service_role key, an
`Authorization` header without the `Bearer` scheme, a JWT with no `kid`,
an expired token, a JWKS from the wrong project.
The middleware that answer directly get the same treatment rather than
their own hand-rolled bodies: `withClaims` / `withRequiredClaims` report
`MISSING_JWKS`, `MISSING_CREDENTIALS`, `INVALID_JWT`;
`withPostgresClient` / `withPostgresAdminClient` report
`MISSING_CONNECTION_STRING` and a catalogued `UNSUPPORTED_ROLE`.
`details` never carries key values or token payloads: API keys are
reported by prefix format, named keys by name, JWTs by `alg`/`kid` only.
Note: server misconfiguration now surfaces as 500 rather than 401. A
missing or unreachable JWKS, or an auth mode no configured key can match,
are not the caller's fault.
* feat: add `errors: { detailed: false }` to trim the error response body
`hint`, `docs`, and `details` are written for whoever is building against
the endpoint, and not everyone wants them on the wire. `errors.detailed`
(default `true`) reduces the body to `code` and `message` alone.
Provenance survives the trim: `message` keeps its `[@supabase/server]`
prefix, and the code is still sent as the `x-supabase-server-error`
header — so the error stays identifiable without the `source` field.
Response-only. The HTTP status is unaffected and the error object keeps
`hint`, `docs`, and `details` in full, so `createSupabaseContext` callers
and the framework adapters see everything.
Documented as a verbosity control rather than a security boundary — `code`
and `message` still name the failure specifically. Formatting the response
by hand via `createSupabaseContext` remains the way to disclose nothing.
* feat: distinguish UNUSABLE_CREDENTIAL from MISSING_CREDENTIALS
Review feedback on #130: the top-level code was `MISSING_CREDENTIALS`
even when a credential had arrived, just the wrong kind. `received.
authorization: 'api-key'` and the hint carried the diagnosis, but
`errors: { detailed: false }` strips both — leaving a caller who is
demonstrably sending a key staring at a bare `MISSING_CREDENTIALS`.
That mode makes the code the only thing a caller can rely on, so it has
to be true standing alone. `UNUSABLE_CREDENTIAL` (401) now covers "a
credential arrived that no accepted mode can use", partitioning the space
exactly against `MISSING_CREDENTIALS` ("nothing arrived"). It has two
shapes, named in the `message` so the diagnosis survives the trim:
- wrong kind: an `sb_*` API key in the Authorization header
- unreadable: wrong scheme, wrong casing, bare value, empty token
The unreadable shapes had the same defect and are fixed with it — a
`Basic` or lowercase-`bearer` header is not a missing credential either.
Classification moves into one shared `diagnoseAuthorizationHeader`, since
only the raw header separates "sent nothing" from "sent something
unreadable" and both `verifyAuth` and the `withRequiredClaims` gate need
that distinction. Previously the gate could not make it at all, so the
two disagreed on every scheme case. A parity matrix over all six header
shapes now pins gate and `withSupabase({ auth: 'user' })` to the same
status and code.
* fix: preserve error cause when client creation fails
* fix: report API keys as UNUSABLE_CREDENTIAL on user-only endpoints
Review feedback on #130: `supabase-js` sends the publishable key in both
the `apikey` and `Authorization` headers, so an unauthenticated browser
call to an `auth: 'user'` endpoint arrives with a key in each slot. The
`apikey !== 'absent'` branch in `explainFallthrough` was read first, so
the caller got `INVALID_API_KEY` — "check you are pointing at the right
Supabase project" — for a key that was never going to be looked up. With
`errors: { detailed: false }` the code is all they get, and it sent them
hunting for a key mismatch that does not exist.
`INVALID_API_KEY` means "matched none of the configured keys", which only
says something when a mode was doing that lookup. It is now gated on an
attempted `publishable` / `secret` mode; where no mode reads keys, a key
in either header is `UNUSABLE_CREDENTIAL` — not wrong, just the wrong
kind of credential. The apikey-header-only case had the same defect and
is fixed with it: "matched no key configured for auth mode(s): "user""
described a lookup that never happened.
The new diagnosis is shared as `apiKeyOnUserOnlyEndpoint`, so the
`withRequiredClaims` gate stops answering with "API keys belong in the
`apikey` header" for callers who already sent it there — that gate only
ever accepts a user JWT, so moving the key would not help. It keeps the
parity the gate is built for: an identical request, worded identically
from both paths. `ApiKeyInAuthorizationHeader` still covers the case
where the advice is right — a mixed `['user', 'publishable']` endpoint
with a key in `Authorization` alone.
* docs: add MissingConnectionStringError documentation and clarify credential error handling
24 KiB
API Reference
Complete reference for every export, organized by entry point.
@supabase/server
withSupabase
function withSupabase<Database = unknown>(
config: WithSupabaseConfig,
handler: (req: Request, ctx: SupabaseContext<Database>) => Promise<Response>,
): (req: Request) => Promise<Response>
Wraps a fetch handler with auth, CORS, and client creation. Returns a (req: Request) => Promise<Response> function suitable for export default { fetch }.
- Handles
OPTIONSpreflight when CORS is enabled - Verifies credentials per
config.auth - Returns JSON error response on auth failure
- Adds CORS headers to all responses
createSupabaseContext
function createSupabaseContext<Database = unknown>(
request: Request,
options?: WithSupabaseConfig,
): Promise<
| { data: SupabaseContext<Database>; error: null }
| { data: null; error: AuthError }
>
Creates a SupabaseContext from a request. Returns a result tuple. The cors option is ignored.
Defaults to auth: 'user' when options is omitted.
@supabase/server/core
verifyAuth
function verifyAuth(
request: Request,
options: {
auth?: AuthModeWithKey | AuthModeWithKey[]
env?: Partial<SupabaseEnv>
},
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
Extracts credentials from a request and verifies them. Convenience wrapper over extractCredentials + verifyCredentials.
verifyCredentials
function verifyCredentials(
credentials: Credentials,
options: {
auth?: AuthModeWithKey | AuthModeWithKey[]
env?: Partial<SupabaseEnv>
},
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
Verifies pre-extracted credentials against allowed auth modes. Tries each mode in order — first match wins.
extractCredentials
function extractCredentials(request: Request): Credentials
Reads Authorization: Bearer <token> and apikey headers from a request. Pure extraction, no validation. Synchronous.
resolveEnv
function resolveEnv(
overrides?: Partial<SupabaseEnv>,
): { data: SupabaseEnv; error: null } | { data: null; error: EnvError }
Resolves Supabase environment configuration from runtime variables. SUPABASE_URL is the only hard requirement.
createContextClient
function createContextClient<Database = unknown>(
options?: CreateContextClientOptions,
): SupabaseClient<Database>
Creates a user-scoped Supabase client. RLS applies. Throws EnvError if URL or publishable key is missing.
Configured with:
- Publishable key (named or default) as
apikeyheader - User's JWT as
Authorization: Bearerheader (whenauth.tokenis provided) persistSession: false,autoRefreshToken: false,detectSessionInUrl: false
createAdminClient
function createAdminClient<Database = unknown>(
options?: CreateAdminClientOptions,
): SupabaseClient<Database>
Creates an admin Supabase client that bypasses RLS. Throws EnvError if URL or secret key is missing.
@supabase/server/adapters/hono
withSupabase (Hono)
function withSupabase(
config?: Omit<WithSupabaseConfig, 'cors'>,
): MiddlewareHandler
Hono middleware. Sets c.var.supabaseContext on the Hono context. Throws HTTPException on auth failure with cause: AuthError.
Skips if c.var.supabaseContext is already set (enables route-level overrides).
Defaults to auth: 'user' when config is omitted.
@supabase/server/adapters/h3
withSupabase (H3)
function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware
H3 middleware. Sets event.context.supabaseContext on the H3 event. Throws HTTPError on auth failure with cause: AuthError.
Skips if event.context.supabaseContext is already set (enables chained middleware).
Defaults to auth: 'user' when config is omitted.
@supabase/server/adapters/elysia
withSupabase (Elysia)
function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Elysia
Elysia plugin that resolves supabaseContext into the request context. Throws an error on auth failure with cause: AuthError.
Skips if supabaseContext is already resolved by a prior plugin.
Defaults to auth: 'user' when config is omitted.
@supabase/server/middleware/claims
withClaims
const withClaims: Middleware<
'jwtClaims',
WithClaimsConfig | void,
Record<never, never>,
JWTClaims | null
>
Contributes ctx.jwtClaims by verifying the caller's Bearer token against the project JWKS. This is the same verification core withSupabase uses for its user auth mode.
Behavior:
- No
Authorization: Bearertoken, or ansb_*API key in that position: contributesnulland the request proceeds as anonymous. - Token present but invalid: short-circuits with a 401 and code
INVALID_JWT, naming the specific reason (expired, bad signature, unknownkid, malformed, nosub). - Token present but no JWKS configured: short-circuits with a 500 and code
JWKS_NOT_CONFIGURED— the same codewithSupabase'susermode reports, with ahintnaming this middleware'sjwksoption. Verification is required; the middleware has no decode-only mode. - Remote JWKS unreachable: short-circuits with a 500 and code
JWKS_FETCH_FAILED.
Responses use the standard error payload.
withClaims is not an auth gate. It never rejects a request that has no token, so [withClaims(), withSupabaseClient()] is not the composable form of withSupabase({ auth: 'user' }) and accepts anonymous callers. To require an authenticated caller, compose withRequiredClaims (@supabase/server/middleware/required-claims) instead. The two entries share the jwtClaims key, so a pipeline picks "claims if present" or "claims required"; composing both is a compile-time conflict.
WithClaimsConfig
interface WithClaimsConfig {
jwks?: JSONWebKeySet | URL
}
Defaults to SUPABASE_JWKS (inline JSON) or SUPABASE_JWKS_URL (https endpoint) from the environment.
@supabase/server/middleware/required-claims
withRequiredClaims
const withRequiredClaims: Middleware<
'jwtClaims',
WithRequiredClaimsConfig | void,
Record<never, never>,
JWTClaims
>
The user-mode auth gate. Verifies the caller's Bearer token against the project JWKS and contributes non-null ctx.jwtClaims. This is the same verification core withSupabase uses for its user auth mode.
Behavior:
- No
Authorizationheader: short-circuits with a 401 and codeMISSING_CREDENTIALS. The handler never runs. - An
sb_*API key in theAuthorizationheader: a 401 with codeUNUSABLE_CREDENTIAL— a credential arrived, just not a user JWT. - Token present but invalid: a 401 with code
INVALID_JWT, naming the specific reason. - Token present but no JWKS configured: short-circuits with a 500 and code
JWKS_NOT_CONFIGURED— the same codewithSupabase'susermode reports, with ahintnaming this middleware'sjwksoption. Verification is required; the middleware has no decode-only mode. - Remote JWKS unreachable: short-circuits with a 500 and code
JWKS_FETCH_FAILED.
Responses use the standard error payload.
withRequiredClaims is the required-caller counterpart to withClaims: "claims required" rather than "claims if present". The two share the jwtClaims key, so composing both in one pipeline is a compile-time conflict.
Because the contribution is non-null, gated handlers read ctx.jwtClaims directly, and entries declaring a jwtClaims prerequisite, such as withPostgresClient, compose with no further verification:
pipeline([withRequiredClaims(), withPostgresClient()], async (req, ctx) => {
const rows = await ctx.postgres.query`select id, title from posts`
return Response.json({ rows, caller: ctx.jwtClaims.sub })
})
The gate's 401 and 500 short-circuits carry no CORS headers, and a bare pipeline answers no OPTIONS preflight. For browser callers, compose withCors (@supabase/middleware/cors) ahead of the gate: it answers preflight before the gate runs and stamps Access-Control-* headers on the gate's short-circuit responses.
Inside withSupabase the context already carries verified jwtClaims, so composing the gate through the middleware option is a compile-time conflict. Use withSupabase({ auth: 'user' }) to gate that path.
The gate contributes jwtClaims and nothing else. A handler that needs the full SupabaseContext behind an auth gate (for example ctx.userClaims or ctx.authMode, which no composable entry contributes) uses withSupabase({ auth: 'user' }) directly. A host that takes an entries array can wrap it as the sole entry. cors: 'disabled' leaves CORS handling to the host:
const entry = (h: (req: Request, ctx: object) => Promise<Response>) =>
withSupabase({ auth: 'user', cors: 'disabled' }, h)
WithRequiredClaimsConfig
interface WithRequiredClaimsConfig {
jwks?: JSONWebKeySet | URL
}
Defaults to SUPABASE_JWKS (inline JSON) or SUPABASE_JWKS_URL (https endpoint) from the environment.
@supabase/server/middleware/postgres
withPostgresClient
const withPostgresClient: Middleware<
'postgres',
WithPostgresClientConfig | void,
{ jwtClaims: RequestClaims | null },
PostgresApi
>
Contributes ctx.postgres — a pg client scoped to the caller by RLS. Each query runs in its own transaction that sets request.jwt.claims and drops to the caller's role before the statement, so auth.uid() resolves and policies enforce.
Only authenticated and anon are assumed. A verified token naming any other role — service_role or a custom role — short-circuits with a 500 and { message, code: 'UNSUPPORTED_ROLE' } naming the role, rather than being downgraded to anon. A missing or absent role claim is anon.
Requires ctx.jwtClaims upstream — supplied by withSupabase or by withClaims in a standalone pipeline. Composing it without one is a compile-time error.
Short-circuits with a 500 and code MISSING_CONNECTION_STRING when no connection string is available.
Needs raw TCP: Node, Deno, Bun, and the Supabase Edge runtime, not Workers-style isolates. pg is an optional peer dependency.
See docs/postgres.md.
PostgresApi
interface PostgresApi {
query<T = Record<string, unknown>>(
strings: TemplateStringsArray,
...values: unknown[]
): Promise<T[]>
queryRaw<T = Record<string, unknown>>(
text: string,
params?: unknown[],
): Promise<T[]>
}
The value at ctx.postgres. Both methods return the result rows directly (not a pg Result).
query is a tagged template, so every interpolation becomes a bind parameter and can never alter the statement:
const rows = await ctx.postgres
.query`select id, body from notes where id = ${id}`
// -> select id, body from notes where id = $1 with values [id]
Tagged templates cannot carry type arguments, so annotate the binding instead of writing query<NoteRow>:
const rows: NoteRow[] = await ctx.postgres.query`select id, body from notes`
Passing a plain string to query throws — the two calls differ only in their brackets, so it refuses rather than silently reinterpreting.
queryRaw takes SQL text plus params, for text that cannot be a literal: a query builder emitting { sql, parameters }, or SQL that must interpolate an identifier. Identifiers can never be bind parameters, so check them against a set you control and quote them with ident:
import { ident } from '@supabase/server/middleware/postgres'
const SORTABLE = new Set(['created_at', 'title'])
if (!SORTABLE.has(column)) throw new Error('unsupported sort column')
const rows = await ctx.postgres.queryRaw(
`select id, title from posts order by ${ident(column)} desc`,
)
ident quotes and escapes, but does not authorize — it stops injection, not a caller reading a column they should not see. The allowlist is what does that.
WithPostgresClientConfig
interface WithPostgresClientConfig {
connectionString?: string
}
Defaults to the SUPABASE_DB_URL environment variable. Pools are created lazily, one per connection string per process.
RequestClaims
interface RequestClaims {
role?: string
[key: string]: unknown
}
The minimal claims shape withPostgresClient requires upstream at ctx.jwtClaims. Satisfied by withSupabase's JWKS-verified claims and by withClaims. Only role is read; the whole object is serialized into request.jwt.claims.
@supabase/server/middleware/postgres-admin
withPostgresAdminClient
const withPostgresAdminClient: Middleware<
'postgresAdmin',
WithPostgresAdminClientConfig | void,
Record<never, never>,
PostgresApi
>
Contributes ctx.postgresAdmin — a pg client that bypasses RLS. Queries run as-is, as the role in the connection string: no claim injection, no role switching, no wrapping transaction.
Declares no upstream prerequisite, so it composes in any auth mode including 'secret' and 'none'. Shares the pool cache with withPostgresClient — same connection string, one pool.
Short-circuits with a 500 and code MISSING_CONNECTION_STRING when no connection string is available.
Authorization is the caller's responsibility: RLS is not consulted, so per-user scoping must be an explicit where clause.
WithPostgresAdminClientConfig
interface WithPostgresAdminClientConfig {
connectionString?: string
}
Defaults to the SUPABASE_DB_URL environment variable.
Types
AuthMode
type AuthMode = 'none' | 'publishable' | 'secret' | 'user'
AuthModeWithKey
type AuthModeWithKey = AuthMode | `publishable:${string}` | `secret:${string}`
Extended auth mode with named key support. Examples: 'publishable:web', 'secret:*', 'secret:internal'. The bare form ('publishable' / 'secret') matches only the default key; :* accepts any key in the set.
Allow / AllowWithKey (deprecated aliases)
Allow and AllowWithKey are kept as deprecated aliases for AuthMode and AuthModeWithKey. Prefer the Auth* names — the legacy ones will be removed in a future major release.
SupabaseContext<Database>
interface SupabaseContext<Database = unknown> {
supabase: SupabaseClient<Database>
supabaseAdmin: SupabaseClient<Database>
userClaims: UserClaims | null
jwtClaims: JWTClaims | null
authMode: AuthMode
authKeyName?: string
}
WithSupabaseConfig
interface WithSupabaseConfig {
auth?: AuthModeWithKey | AuthModeWithKey[] // default: 'user'
/** @deprecated use `auth` instead — will be removed in a future major release */
allow?: AuthModeWithKey | AuthModeWithKey[]
env?: Partial<SupabaseEnv>
cors?: boolean | Record<string, string> // default: true
supabaseOptions?: SupabaseClientOptions<string>
errors?: ErrorResponseConfig
}
ErrorResponseConfig
interface ErrorResponseConfig {
detailed?: boolean // default: true
}
detailed: false reduces the error response body to code and message alone, dropping source, hint, docs, and details. The status and x-supabase-server-error header are unaffected, and the error object itself keeps everything. See error-handling.md.
SupabaseEnv
interface SupabaseEnv {
url: string
publishableKeys: Record<string, string>
secretKeys: Record<string, string>
jwks: JsonWebKeySet | null
}
Credentials
interface Credentials {
token: string | null
apikey: string | null
}
AuthResult
interface AuthResult {
authMode: AuthMode
token: string | null
userClaims: UserClaims | null
jwtClaims: JWTClaims | null
keyName?: string | null
}
JWTClaims
interface JWTClaims {
sub: string
iss?: string
aud?: string | string[]
exp?: number
iat?: number
role?: string
email?: string
app_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
[key: string]: unknown
}
UserClaims
interface UserClaims {
id: string
role?: string
email?: string
appMetadata?: Record<string, unknown>
userMetadata?: Record<string, unknown>
}
ClientAuth
interface ClientAuth {
token?: string | null
keyName?: string | null
}
CreateContextClientOptions
interface CreateContextClientOptions {
auth?: ClientAuth
env?: Partial<SupabaseEnv>
supabaseOptions?: SupabaseClientOptions<string>
}
CreateAdminClientOptions
interface CreateAdminClientOptions {
auth?: Pick<ClientAuth, 'keyName'>
env?: Partial<SupabaseEnv>
supabaseOptions?: SupabaseClientOptions<string>
}
JsonWebKeySet
interface JsonWebKeySet {
keys: JsonWebKey[]
}
Peer Dependencies
Some peer dependencies types are available from @supabase/server/peer/* export
supabase-js
Only a curated set of types are available to import — It means that may be missing types from the original lib.
import type {
SupabaseClient,
PostgrestError,
AuthError as SupabaseAuthError, // Avoid clashing with this SDK's own `AuthError` class.
// ...
} from '@supabase/server/peer/supabase-js'
Error Classes
SupabaseServerError
Base class for every error the library produces — catch this to handle anything from @supabase/server.
abstract class SupabaseServerError extends Error {
readonly source: '@supabase/server'
abstract readonly status: number
readonly code: string
readonly hint?: string // actionable next step
readonly docs: string // link to docs/error-handling.md#<code>
readonly details?: Record<string, unknown> // non-sensitive diagnostics
toJSON(): ErrorPayload
}
message is always prefixed [@supabase/server]. details never contains key values or token payloads. toJSON() is picked up by JSON.stringify, so logging the error yields the full diagnostics.
EnvError
class EnvError extends SupabaseServerError {
readonly status: 500
constructor(
message: string,
code?: string,
options?: SupabaseServerErrorOptions,
)
}
AuthError
class AuthError extends SupabaseServerError {
readonly status: number // 401 = bad credentials, 500 = server misconfigured
constructor(
message: string,
code?: string,
status?: number,
options?: SupabaseServerErrorOptions,
)
}
ErrorPayload
The JSON body every auto-responding layer returns, and the return type of toJSON().
interface ErrorPayload {
source: '@supabase/server'
code: string
message: string
hint?: string
docs: string
details?: Record<string, unknown>
}
SupabaseServerErrorOptions
interface SupabaseServerErrorOptions {
hint?: string
details?: Record<string, unknown>
docs?: string // overrides the generated URL
cause?: unknown
}
Error Code Constants
| Constant | Value | Class | Meaning |
|---|---|---|---|
EnvGenericError |
'ENV_ERROR' |
EnvError |
Generic environment error |
MissingSupabaseURLError |
'MISSING_SUPABASE_URL' |
EnvError |
SUPABASE_URL not set |
MissingPublishableKeyError |
'MISSING_PUBLISHABLE_KEY' |
EnvError |
Named publishable key not found |
MissingDefaultPublishableKeyError |
'MISSING_DEFAULT_PUBLISHABLE_KEY' |
EnvError |
No default publishable key |
MissingSecretKeyError |
'MISSING_SECRET_KEY' |
EnvError |
Named secret key not found |
MissingDefaultSecretKeyError |
'MISSING_DEFAULT_SECRET_KEY' |
EnvError |
No default secret key |
MissingResourceServerError |
'MISSING_RESOURCE_SERVER' |
EnvError |
withOAuthProtectedResource cannot derive a resourceServer |
MissingAuthorizationServerError |
'MISSING_AUTHORIZATION_SERVER' |
EnvError |
withOAuthProtectedResource cannot derive an authorization server |
MissingConnectionStringError |
'MISSING_CONNECTION_STRING' |
EnvError |
No Postgres connection string configured |
AuthGenericError |
'AUTH_ERROR' |
AuthError |
Generic auth error (401) |
MissingCredentialsError |
'MISSING_CREDENTIALS' |
AuthError |
Request carried no credentials at all (401) |
UnusableCredentialError |
'UNUSABLE_CREDENTIAL' |
AuthError |
A credential arrived but cannot be used (401) |
InvalidApiKeyError |
'INVALID_API_KEY' |
AuthError |
apikey matched no configured key (401) |
InvalidJwtError |
'INVALID_JWT' |
AuthError |
JWT failed verification (401) |
InvalidCredentialsError |
'INVALID_CREDENTIALS' |
AuthError |
Fallback credential failure (401) |
JwksNotConfiguredError |
'JWKS_NOT_CONFIGURED' |
AuthError |
JWT sent but no JWKS configured (500) |
JwksFetchFailedError |
'JWKS_FETCH_FAILED' |
AuthError |
Remote JWKS unreachable or unusable (500) |
NoKeysConfiguredError |
'NO_KEYS_CONFIGURED' |
AuthError |
Auth mode no configured key can match (500) |
UnsupportedRoleError |
'UNSUPPORTED_ROLE' |
AuthError |
withPostgresClient will not assume the caller's role claim (500) |
CreateSupabaseClientError |
'CREATE_SUPABASE_CLIENT_ERROR' |
AuthError |
Client creation failed after auth (500) |
Also exported: ErrorSource ('@supabase/server') and ErrorCodeHeader ('x-supabase-server-error').
See error-handling.md for the meaning, hint, and details of each code.
Errors Factory Map
const Errors: {
[MissingSupabaseURLError]: () => EnvError
[MissingPublishableKeyError]: (name, configuredKeyNames?) => EnvError
[MissingDefaultPublishableKeyError]: (configuredKeyNames?) => EnvError
[MissingSecretKeyError]: (name, configuredKeyNames?) => EnvError
[MissingDefaultSecretKeyError]: (configuredKeyNames?) => EnvError
[MissingResourceServerError]: () => EnvError
[MissingAuthorizationServerError]: () => EnvError
[MissingConnectionStringError]: (middleware: string) => EnvError
[MissingCredentialsError]: (context: AuthFailureContext) => AuthError
[UnusableCredentialError]: (
context: PartialContext & { reason; hint },
) => AuthError
[InvalidApiKeyError]: (context: AuthFailureContext) => AuthError
[InvalidJwtError]: (context: PartialContext & JwtFailure) => AuthError
[InvalidCredentialsError]: (context?: AuthFailureContext) => AuthError
[JwksNotConfiguredError]: (
context?: PartialContext & { middleware? },
) => AuthError
[JwksFetchFailedError]: (context: PartialContext & { reason }) => AuthError
[NoKeysConfiguredError]: (
context: AuthFailureContext & { mode; keyKind },
) => AuthError
[UnsupportedRoleError]: (context: {
requestedRole
supportedRoles
}) => AuthError
[CreateSupabaseClientError]: (options?: { cause?: unknown }) => AuthError
}
Keyed by error code constant. Each entry returns an error pre-configured with hint, docs, and non-sensitive details. The named-key factories accept the configured key names so they can be reported in the message without exposing key values.
AuthFailureContext
Non-sensitive diagnostics the auth pipeline passes to the factories.
interface AuthFailureContext {
authModes: readonly string[]
received: {
authorization: 'bearer' | 'api-key' | 'non-bearer-scheme' | 'absent'
apikey: 'absent' | 'publishable' | 'secret' | 'legacy-jwt' | 'unrecognized'
}
configuredKeyNames?: Record<string, readonly string[]>
}