Files
supabase__server/docs/environment-variables.md
T
Raúl Barroso 11cce29d1a feat: withOAuthProtectedResource url config (#117)
Make the middleware work off Supabase Edge Functions. Previously it
hardcoded the Edge Functions proxy shape into its URL derivation, which is
wrong anywhere else, and took no configuration to correct it.

Adds `resourceServer` and `authorizationServer`, each a fixed string or a
function of the request, plus a `fromSupabaseUrl` adapter. Edge Functions
stays zero-config; off it, an unset option throws rather than advertising a
request-derived guess. `authorizationServer` falls back to
`SUPABASE_PUBLIC_URL` then `SUPABASE_URL` first, both rungs below the Edge
derivation so an internal gateway host can never displace the origin the
client used.

Also uses `SUPABASE_FUNCTION_SLUG` for a canonical `/functions/v1/{slug}`,
fixes three header-parsing bugs (comma-joined value, double port,
case-sensitive proto), and makes the advertised `resource` always equal the
URL the client called, as RFC 9728 §3.3 requires.
2026-08-26 18:47:23 +02:00

8.5 KiB

Supabase environments (zero config)

On Supabase Platform and Local Development (CLI), all variables are auto-provisioned — no configuration needed

Variable Format Description Available in
SUPABASE_URL https://<ref>.supabase.co Your Supabase project URL All
SUPABASE_PUBLISHABLE_KEYS {"default":"sb_publishable_..."} Named publishable keys as JSON object All
SUPABASE_SECRET_KEYS {"default":"sb_secret_..."} Named secret keys as JSON object All
SUPABASE_JWKS {"keys":[...]} or [...] Inline JSON Web Key Set for JWT verification All
SUPABASE_PUBLISHABLE_KEY sb_publishable_... Single publishable key (fallback) Self-hosted, if manually exported
SUPABASE_SECRET_KEY sb_secret_... Single secret key (fallback) Self-hosted, if manually exported
SUPABASE_PUBLIC_URL https://<ref>.supabase.co Externally-visible URL of the Supabase stack. Preferred origin for OAuth protected resource metadata Self-hosted
SUPABASE_FUNCTION_SLUG my-function The running function's slug. Yields a canonical /functions/v1/{slug} resource identifier with no path parsing Edge Functions

Non-Supabase environments (Node.js, Bun, Cloudflare, self-hosted)

Set these based on which auth modes your app uses:

Variable Required when
SUPABASE_URL Always. Also the last-resort authorizationServer for withOAuthProtectedResource
SUPABASE_SECRET_KEY auth: 'secret', or when the handler accesses supabaseAdmin
SUPABASE_PUBLISHABLE_KEY auth: 'publishable'
SUPABASE_JWKS or SUPABASE_JWKS_URL auth: 'user' (JWT verification)

Minimal .env example

SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SECRET_KEY=sb_secret_...
SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_JWKS={"keys":[...]}

Plural vs singular keys

The SDK checks the plural form first (SUPABASE_PUBLISHABLE_KEYS), then falls back to the singular form (SUPABASE_PUBLISHABLE_KEY). The same applies to secret keys.

Plural form — named keys as a JSON object

Use this when you have multiple keys for different clients (web, mobile, internal):

SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc","web":"sb_publishable_web_xyz","mobile":"sb_publishable_mobile_123"}
SUPABASE_SECRET_KEYS={"default":"sb_secret_default_abc","internal":"sb_secret_internal_xyz"}

You can then validate against specific keys with named key syntax:

// Only accept the "web" publishable key
withSupabase({ auth: 'publishable:web' }, handler)

// Accept any secret key
withSupabase({ auth: 'secret:*' }, handler)

Singular form — equivalent to a single "default" key

SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
SUPABASE_SECRET_KEY=sb_secret_default_abc

This is equivalent to setting the plural form with a single "default" entry:

# These two are the same:
SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc"}

The singular form is a convenience for the common case where you only have one key. The SDK stores it internally as { default: "<value>" }, so auth: 'publishable' (which looks for the "default" key) works with both forms.

Priority

When both singular and plural forms are set, the plural form takes priority.

JWKS source

JWT verification (auth: 'user') needs a JWKS. There are two ways to provide one:

# Inline JSON — standard JWKS format
SUPABASE_JWKS={"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}

# Inline JSON — bare array (convenience, wrapped as { keys: [...] })
SUPABASE_JWKS=[{"kty":"RSA","n":"...","e":"AQAB"}]

# Remote JWKS endpoint — keys are fetched on demand and cached in memory.
# HTTPS is required for any non-loopback host; plain http:// is rejected
# (a MITM on the JWKS fetch could swap in an attacker-controlled key and
# forge JWTs that verify). http:// is allowed for loopback hosts only —
# `localhost`, `127.0.0.0/8`, `::1` — to support the local Supabase CLI.
SUPABASE_JWKS_URL=https://<ref>.supabase.co/auth/v1/.well-known/jwks.json

# Local development against `supabase start`:
SUPABASE_JWKS_URL=http://localhost:54321/auth/v1/.well-known/jwks.json

Resolution order

  1. SUPABASE_JWKS — when set, treated as authoritative inline JSON.
  2. SUPABASE_JWKS_URL — only checked when SUPABASE_JWKS is unset or empty. Must be https://, except loopback hosts may use http://.
  3. Otherwise — null. JWT verification (auth: 'user') is unavailable.

Runtime-specific behavior

The SDK reads environment variables using this priority:

  1. Deno.env.get(name) — Deno (including Supabase Edge Functions)
  2. process.env[name] — Node.js, Bun, Cloudflare Workers (with node-compat)

Supabase Edge Functions

Environment variables are auto-provisioned by the platform. Nothing to configure.

Deno / Node.js / Bun

Set variables via .env files (with a loader like dotenv for Node.js) or your deployment platform's environment configuration.

Cloudflare Workers

Cloudflare Workers don't expose Deno.env or process.env by default. Two options:

  1. Enable node-compat in wrangler.toml:

    compatibility_flags = ["nodejs_compat"]
    
  2. Pass overrides via the env config option:

    withSupabase(
      {
        auth: 'user',
        env: {
          url: env.SUPABASE_URL,
          publishableKeys: { default: env.SUPABASE_PUBLISHABLE_KEY },
          secretKeys: { default: env.SUPABASE_SECRET_KEY },
        },
      },
      handler,
    )
    

Using env overrides

The env option on withSupabase, createSupabaseContext, and core primitives lets you override auto-detected values. Partial overrides are merged with what's resolved from environment variables:

import { withSupabase } from '@supabase/server'

export default {
  fetch: withSupabase(
    {
      auth: 'user',
      env: {
        url: 'http://localhost:54321', // override just the URL
      },
    },
    handler,
  ),
}

Using resolveEnv directly

For manual environment resolution — useful in tests, custom setups, or debugging:

import { resolveEnv } from '@supabase/server/core'

const { data: env, error } = resolveEnv()
if (error) {
  console.error(`Missing config: ${error.message}`)
}

// With overrides
const { data: envOverridden } = resolveEnv({
  url: 'http://localhost:54321',
  publishableKeys: { default: 'test-key' },
})

resolveEnv returns a SupabaseEnv object:

interface SupabaseEnv {
  url: string
  publishableKeys: Record<string, string>
  secretKeys: Record<string, string>
  // `URL` when SUPABASE_JWKS is a remote endpoint, `JsonWebKeySet` for inline keys
  jwks: JsonWebKeySet | URL | null
}

Graceful parsing

Malformed JSON in environment variables doesn't throw — the SDK falls back to empty values:

  • Malformed SUPABASE_PUBLISHABLE_KEYS or SUPABASE_SECRET_KEYS → empty {}
  • Malformed SUPABASE_JWKSnull (JWT verification unavailable)
  • Missing SUPABASE_URLEnvError (this is the only hard requirement)