Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
| `@supabase/server/middleware/claims` | `withClaims` (JWKS-verified `ctx.jwtClaims`) |
| `@supabase/server/middleware/postgres` | `withPostgresClient` (RLS-scoped `ctx.postgres` client) |
| `@supabase/server/middleware/postgres-admin` | `withPostgresAdminClient` (`ctx.postgresAdmin`, bypasses RLS) |
| `@supabase/server/oauth-protected-resource` | `withOAuthProtectedResource`, `resourceMetadataResponse`, `unauthorizedResponse` |
| `@supabase/server/oauth-protected-resource` | `withOAuthProtectedResource`, `fromSupabaseUrl`, `resourceMetadataResponse`, `unauthorizedResponse` |
| `@supabase/server/peer/supabase-js` | Re-exported `supabase-js` types (`SupabaseClient`, `PostgrestError`, …) |

## Documentation
Expand Down
30 changes: 16 additions & 14 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@

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 |
| 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 |
| `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) |
| 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

Expand Down
39 changes: 21 additions & 18 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ The SDK has two error classes, both with `status` (HTTP code) and `code` (machin

Thrown when a required environment variable is missing or malformed. Always `status: 500` — these are server configuration issues, not client errors.

| Code | Meaning |
| --------------------------------- | -------------------------------------------------------------- |
| `MISSING_SUPABASE_URL` | `SUPABASE_URL` is not set |
| `MISSING_PUBLISHABLE_KEY` | Named publishable key not found in `SUPABASE_PUBLISHABLE_KEYS` |
| `MISSING_DEFAULT_PUBLISHABLE_KEY` | No default publishable key found |
| `MISSING_SECRET_KEY` | Named secret key not found in `SUPABASE_SECRET_KEYS` |
| `MISSING_DEFAULT_SECRET_KEY` | No default secret key found |
| `ENV_ERROR` | Generic environment error |
| Code | Meaning |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `MISSING_SUPABASE_URL` | `SUPABASE_URL` is not set |
| `MISSING_PUBLISHABLE_KEY` | Named publishable key not found in `SUPABASE_PUBLISHABLE_KEYS` |
| `MISSING_DEFAULT_PUBLISHABLE_KEY` | No default publishable key found |
| `MISSING_SECRET_KEY` | Named secret key not found in `SUPABASE_SECRET_KEYS` |
| `MISSING_DEFAULT_SECRET_KEY` | No default secret key found |
| `MISSING_RESOURCE_SERVER` | `withOAuthProtectedResource` has no `resourceServer` and is not on Edge Functions |
| `MISSING_AUTHORIZATION_SERVER` | `withOAuthProtectedResource` has no `authorizationServer`, and neither `SUPABASE_PUBLIC_URL` nor `SUPABASE_URL` is set |
| `ENV_ERROR` | Generic environment error |

### AuthError

Expand All @@ -31,16 +33,17 @@ Thrown when authentication or authorization fails. Status is `401` for invalid c

Different layers of the SDK handle errors differently. Understanding which pattern each function uses prevents surprises.

| Function | Pattern | What happens on error |
| ------------------------- | ------------- | ------------------------------------------------------------------------ |
| `withSupabase()` | Auto-response | Returns `Response.json({ message, code }, { status })` with CORS headers |
| `createSupabaseContext()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `verifyAuth()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `verifyCredentials()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `resolveEnv()` | Result tuple | Returns `{ data: null, error: EnvError }` |
| `createContextClient()` | **Throws** | Throws `EnvError` |
| `createAdminClient()` | **Throws** | Throws `EnvError` |
| Hono `withSupabase()` | HTTPException | Throws `HTTPException` with `cause: AuthError` |
| Function | Pattern | What happens on error |
| ------------------------------ | ------------- | ------------------------------------------------------------------------ |
| `withSupabase()` | Auto-response | Returns `Response.json({ message, code }, { status })` with CORS headers |
| `createSupabaseContext()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `verifyAuth()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `verifyCredentials()` | Result tuple | Returns `{ data: null, error: AuthError }` |
| `resolveEnv()` | Result tuple | Returns `{ data: null, error: EnvError }` |
| `createContextClient()` | **Throws** | Throws `EnvError` |
| `createAdminClient()` | **Throws** | Throws `EnvError` |
| `withOAuthProtectedResource()` | **Throws** | Throws `EnvError` when required off Edge Functions and unconfigured |
| Hono `withSupabase()` | HTTPException | Throws `HTTPException` with `cause: AuthError` |

The two client factory functions (`createContextClient`, `createAdminClient`) are the only ones that throw. Everything else returns a result tuple `{ data, error }`.

Expand Down
26 changes: 25 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export class EnvError extends Error {
*
* @see {@link EnvGenericError}, {@link MissingSupabaseURLError},
* {@link MissingPublishableKeyError}, {@link MissingDefaultPublishableKeyError},
* {@link MissingSecretKeyError}, {@link MissingDefaultSecretKeyError}
* {@link MissingSecretKeyError}, {@link MissingDefaultSecretKeyError},
* {@link MissingResourceServerError}, {@link MissingAuthorizationServerError}
*/
readonly code: string

Expand Down Expand Up @@ -76,6 +77,18 @@ export const MissingSecretKeyError = 'MISSING_SECRET_KEY'
*/
export const MissingDefaultSecretKeyError = 'MISSING_DEFAULT_SECRET_KEY'

/**
* `withOAuthProtectedResource` has no `resourceServer` and cannot derive one.
* @category Errors
*/
export const MissingResourceServerError = 'MISSING_RESOURCE_SERVER'

/**
* `withOAuthProtectedResource` has no `authorizationServer` and cannot derive one.
* @category Errors
*/
export const MissingAuthorizationServerError = 'MISSING_AUTHORIZATION_SERVER'

const EnvErrorMap = {
[MissingSupabaseURLError]: (): EnvError =>
new EnvError(
Expand Down Expand Up @@ -103,6 +116,17 @@ const EnvErrorMap = {
'No default publishable key found. Set SUPABASE_PUBLISHABLE_KEY or include a "default" entry in SUPABASE_PUBLISHABLE_KEYS.',
MissingDefaultPublishableKeyError,
),

[MissingResourceServerError]: (): EnvError =>
new EnvError(
"resourceServer is required outside Supabase Edge Functions. Pass it to withOAuthProtectedResource(), e.g. { resourceServer: (req) => new URL(req.url).origin + '/api/mcp' }.",
MissingResourceServerError,
),
[MissingAuthorizationServerError]: (): EnvError =>
new EnvError(
"authorizationServer is required outside Supabase Edge Functions. Pass it to withOAuthProtectedResource() — use fromSupabaseUrl('https://<ref>.supabase.co') for Supabase Auth — or set SUPABASE_URL.",
MissingAuthorizationServerError,
),
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export { withSupabase } from './with-supabase.js'
export { createSupabaseContext } from './create-supabase-context.js'

export { withOAuthProtectedResource } from './oauth-protected-resource/with-oauth-protected-resource.js'
export type {
OAuthProtectedResourceConfig,
OAuthProtectedResourceContribution,
} from './oauth-protected-resource/with-oauth-protected-resource.js'
export { fromSupabaseUrl } from './oauth-protected-resource/url.js'
export type { UrlOption } from './oauth-protected-resource/url.js'
export {
resourceMetadataResponse,
unauthorizedResponse,
Expand Down Expand Up @@ -122,9 +128,11 @@ export {
EnvGenericError,
Errors,
InvalidCredentialsError,
MissingAuthorizationServerError,
MissingDefaultPublishableKeyError,
MissingDefaultSecretKeyError,
MissingPublishableKeyError,
MissingResourceServerError,
MissingSecretKeyError,
MissingSupabaseURLError,
UnsupportedRoleError,
Expand Down
6 changes: 6 additions & 0 deletions src/oauth-protected-resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
*/

export { withOAuthProtectedResource } from './with-oauth-protected-resource.js'
export type {
OAuthProtectedResourceConfig,
OAuthProtectedResourceContribution,
} from './with-oauth-protected-resource.js'
export { fromSupabaseUrl } from './url.js'
export type { UrlOption } from './url.js'
export { resourceMetadataResponse, unauthorizedResponse } from './responses.js'
export type {
ResourceMetadataOptions,
Expand Down
24 changes: 24 additions & 0 deletions src/oauth-protected-resource/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Supabase platform URL path prefixes used when reconstructing a resource's
* external URLs. Kept in one place so the magic strings have a single source of
* truth across the URL derivation and the middleware.
*
* @module
*/

/**
* Path prefix the Supabase Edge Functions platform proxy strips from the request
* before invoking the function, and which the Edge Functions default restores
* when reconstructing the resource's external path.
*
* @internal
*/
export const EDGE_FUNCTIONS_PATH_PREFIX = '/functions/v1'

/**
* Path prefix of the Supabase Auth API, appended to the project's base URL to
* form the OAuth authorization server URL advertised in the resource metadata.
*
* @internal
*/
export const AUTH_PATH_PREFIX = '/auth/v1'
19 changes: 19 additions & 0 deletions src/oauth-protected-resource/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getEnv, runtimeName } from '@supabase/middleware'

/**
* Whether this request is being served by Supabase Edge Functions.
*
* Gates the request-derived URL defaults. The Edge Functions gateway sets
* `X-Forwarded-*` to the project's externally-visible origin and strips the
* `/functions/v1` prefix; off platform those headers describe the app's own
* origin, which is unrelated to the Supabase project.
*
* True when `SUPABASE_FUNCTION_SLUG` is set, or when the host runtime is Deno.
* A plain Deno server or Deno Deploy therefore reads as Edge Functions.
*
* @internal
*/
export function isEdgeFunctions(): boolean {
if (getEnv('SUPABASE_FUNCTION_SLUG')) return true
return runtimeName === 'deno'
}
Loading
Loading