Skip to content
Open
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
27 changes: 16 additions & 11 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,11 @@ Responses use the standard [error payload](error-handling.md#what-a-failure-look
```ts
interface WithClaimsConfig {
jwks?: JSONWebKeySet | URL
errors?: ErrorResponseConfig
}
```

Defaults to `SUPABASE_JWKS` (inline JSON) or `SUPABASE_JWKS_URL` (https endpoint) from the environment.
`jwks` defaults to `SUPABASE_JWKS` (inline JSON) or `SUPABASE_JWKS_URL` (https endpoint) from the environment. `errors` trims the short-circuit response body; see [`ErrorResponseConfig`](#errorresponseconfig).

---

Expand Down Expand Up @@ -297,10 +298,11 @@ const entry = (h: (req: Request, ctx: object) => Promise<Response>) =>
```ts
interface WithRequiredClaimsConfig {
jwks?: JSONWebKeySet | URL
errors?: ErrorResponseConfig
}
```

Defaults to `SUPABASE_JWKS` (inline JSON) or `SUPABASE_JWKS_URL` (https endpoint) from the environment.
`jwks` defaults to `SUPABASE_JWKS` (inline JSON) or `SUPABASE_JWKS_URL` (https endpoint) from the environment. `errors` trims the short-circuit response body; see [`ErrorResponseConfig`](#errorresponseconfig).

---

Expand Down Expand Up @@ -387,10 +389,11 @@ const rows = await ctx.postgres.queryRaw(
```ts
interface WithPostgresClientConfig {
connectionString?: string
errors?: ErrorResponseConfig
}
```

Defaults to the `SUPABASE_DB_URL` environment variable. Pools are created lazily, one per connection string per process.
`connectionString` defaults to the `SUPABASE_DB_URL` environment variable. Pools are created lazily, one per connection string per process. `errors` trims the short-circuit response body; see [`ErrorResponseConfig`](#errorresponseconfig).

### RequestClaims

Expand Down Expand Up @@ -436,10 +439,11 @@ Authorization is the caller's responsibility: RLS is not consulted, so per-user
```ts
interface WithPostgresAdminClientConfig {
connectionString?: string
errors?: ErrorResponseConfig
}
```

Defaults to the `SUPABASE_DB_URL` environment variable.
`connectionString` defaults to the `SUPABASE_DB_URL` environment variable. `errors` trims the short-circuit response body; see [`ErrorResponseConfig`](#errorresponseconfig).

---

Expand All @@ -463,18 +467,19 @@ function withOAuthProtectedResource(
): FetchHandler
```

OAuth 2.1 Protected Resource behavior (RFC 9728) for the wrapped handler. Answers `GET` and `OPTIONS` on any path ending in `/oauth-protected-resource` with the metadata document and a permissive CORS preflight; adds `WWW-Authenticate: Bearer resource_metadata="…"` to a `401` from below unless the handler already set that header; passes everything else through. Runs before the `withSupabase` gate; placing it directly after `withSupabase` with a credentialed auth mode is refused when the stack is built.
OAuth 2.1 Protected Resource behavior (RFC 9728) for the wrapped handler. Answers `GET` and `OPTIONS` on any path ending in `/oauth-protected-resource` with the metadata document and a permissive CORS preflight; adds `WWW-Authenticate: Bearer resource_metadata="…"` to a `401` from below unless the handler already set that header; passes everything else through. Runs before the `withSupabase` gate; placing it directly after `withSupabase` with a credentialed auth mode is refused when the stack is built. A default URL it cannot derive is answered with the library's JSON error response (500 and `x-supabase-server-error`, see [Error handling](error-handling.md#enverror-codes)); a throw from a configured `resourceServer` or `authorizationServer` function propagates.

Contributes `ctx.oauthProtectedResource.resourceMetadataUrl`, the resolved absolute URL of the metadata document.

### OAuthProtectedResourceConfig

| Option | Type | Default on Supabase Edge Functions | Default elsewhere |
| --------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resourceServer` | `UrlOption` | Public origin from `X-Forwarded-*` (or `SUPABASE_PUBLIC_URL`) + `/functions/v1/{SUPABASE_FUNCTION_SLUG}` | None. Throws `MissingResourceServerError` (`MISSING_RESOURCE_SERVER`). |
| `authorizationServer` | `UrlOption` | Public origin + `/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each + `/auth/v1`. Throws `MissingAuthorizationServerError` (`MISSING_AUTHORIZATION_SERVER`) if neither is set. |
| Option | Type | Default on Supabase Edge Functions | Default elsewhere |
| --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resourceServer` | `UrlOption` | Public origin from `X-Forwarded-*` (or `SUPABASE_PUBLIC_URL`) + `/functions/v1/{SUPABASE_FUNCTION_SLUG}` | None. Short-circuits with a 500 and code `MISSING_RESOURCE_SERVER` (`MissingResourceServerError`). |
| `authorizationServer` | `UrlOption` | Public origin + `/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each + `/auth/v1`. Short-circuits with a 500 and code `MISSING_AUTHORIZATION_SERVER` (`MissingAuthorizationServerError`) if neither is set. |
| `errors` | `ErrorResponseConfig` | `{ detailed: true }` | `{ detailed: true }` |

`UrlOption` is `string | ((req: Request) => string)`. Without `SUPABASE_FUNCTION_SLUG` the resource path is reconstructed from the request path with `/functions/v1` restored; a request at the root path with no slug throws `MissingResourceServerError`.
`UrlOption` is `string | ((req: Request) => string)`. Without `SUPABASE_FUNCTION_SLUG` the resource path is reconstructed from the request path with `/functions/v1` restored; a request at the root path with no slug short-circuits with a 500 and code `MISSING_RESOURCE_SERVER`. `errors` (an [`ErrorResponseConfig`](#errorresponseconfig)) trims the body of those 500s.

### fromSupabaseUrl

Expand Down Expand Up @@ -581,7 +586,7 @@ interface ErrorResponseConfig {
}
```

`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`](error-handling.md#trimming-the-response-body).
`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. Accepted as `errors` by `withSupabase` and by every middleware that answers directly: `withClaims`, `withRequiredClaims`, `withPostgresClient`, `withPostgresAdminClient`, `withOAuthProtectedResource`. See [`error-handling.md`](error-handling.md#trimming-the-response-body).

### SupabaseEnv

Expand Down
38 changes: 21 additions & 17 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Access-Control-Expose-Headers: x-supabase-server-error

The code is repeated in the `x-supabase-server-error` response header, and added to `Access-Control-Expose-Headers` so cross-origin browser code can actually read it.

Every layer that answers a request directly uses this shape: `withSupabase`, and the middleware that short-circuit (`withClaims`, `withRequiredClaims`, `withPostgresClient`). The `@supabase/server/middleware/*` subpaths and `@supabase/server/oauth-protected-resource` are alpha; the error payload documented here is stable either way.
Every layer that answers a request directly uses this shape: `withSupabase`, and the middleware that short-circuit (`withClaims`, `withRequiredClaims`, `withPostgresClient`, `withOAuthProtectedResource`). The `@supabase/server/middleware/*` subpaths and `@supabase/server/oauth-protected-resource` are alpha; the error payload documented here is stable either way.

## Trimming the response body

Expand All @@ -48,6 +48,8 @@ Every layer that answers a request directly uses this shape: `withSupabase`, and
withSupabase({ auth: 'user', errors: { detailed: false } }, handler)
```

Every middleware that answers directly accepts the same option and trims its own short-circuit responses: `withClaims`, `withRequiredClaims`, `withPostgresClient`, `withPostgresAdminClient`, `withOAuthProtectedResource`. The option is per entry; a pipeline passes it to each one.

```
HTTP/1.1 401 Unauthorized
x-supabase-server-error: MISSING_CREDENTIALS
Expand Down Expand Up @@ -245,11 +247,13 @@ Set `SUPABASE_SECRET_KEY`, or add a `"default"` entry to `SUPABASE_SECRET_KEYS`,

### `MISSING_RESOURCE_SERVER`

`withOAuthProtectedResource` is running outside Supabase Edge Functions, where it can't derive the resource URL from the request. Pass `resourceServer` — `hint` shows the shape. `withOAuthProtectedResource` treats the environment as Edge Functions when `SUPABASE_FUNCTION_SLUG` or `SB_EXECUTION_ID` is set, or when the host runtime is Deno. `details.runtime` carries the runtime name the SDK detected.
`withOAuthProtectedResource` is running outside Supabase Edge Functions, where it can't derive the resource URL from the request, so it short-circuits with a 500 on every request: the resource URL backs the metadata document, the `WWW-Authenticate` challenge and `ctx.oauthProtectedResource` alike. Pass `resourceServer` — `hint` shows the shape. `withOAuthProtectedResource` treats the environment as Edge Functions when `SUPABASE_FUNCTION_SLUG` or `SB_EXECUTION_ID` is set, or when the host runtime is Deno. `details.runtime` carries the runtime name the SDK detected.

The escape hatches `resourceMetadataResponse` and `unauthorizedResponse` throw this error rather than returning it.

### `MISSING_AUTHORIZATION_SERVER`

As above for the authorization server. Pass `authorizationServer`, use `fromSupabaseUrl(...)` for Supabase Auth, or set `SUPABASE_PUBLIC_URL` / `SUPABASE_URL`.
As above for the authorization server. Only the metadata document needs it, so the 500 is confined to `GET …/oauth-protected-resource`. Pass `authorizationServer`, use `fromSupabaseUrl(...)` for Supabase Auth, or set `SUPABASE_PUBLIC_URL` / `SUPABASE_URL`.

### `MISSING_CONNECTION_STRING`

Expand All @@ -263,20 +267,20 @@ Generic environment error. The default code when constructing an `EnvError` your

## How errors surface in each layer

| Function | Pattern | What happens on error |
| ------------------------------ | ------------- | ----------------------------------------------------------------------- |
| `withSupabase()` | Auto-response | Returns the JSON payload above, with CORS and `x-supabase-server-error` |
| `withClaims()` | Auto-response | Same payload, short-circuiting the pipeline |
| `withRequiredClaims()` | Auto-response | Same payload, short-circuiting the pipeline |
| `withPostgresClient()` | Auto-response | Same payload, on an unsupported `role` claim |
| `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` |
| Function | Pattern | What happens on error |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------- |
| `withSupabase()` | Auto-response | Returns the JSON payload above, with CORS and `x-supabase-server-error` |
| `withClaims()` | Auto-response | Same payload, short-circuiting the pipeline |
| `withRequiredClaims()` | Auto-response | Same payload, short-circuiting the pipeline |
| `withPostgresClient()` | Auto-response | Same payload, on an unsupported `role` claim |
| `withOAuthProtectedResource()` | Auto-response | Same payload, when a default URL cannot be derived (a configured URL function's throw propagates) |
| `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` |

`verifyAuth()` also has the raw request in hand, so it adds diagnostics `verifyCredentials()` can't see — most usefully, an `Authorization` header that was present but unusable.

Expand Down
16 changes: 9 additions & 7 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The handler is passed inline. Passing a separately declared function typed `(req

## URLs

On Supabase Edge Functions the metadata is derived with no configuration: the public origin from the gateway's `X-Forwarded-*` headers (`SUPABASE_PUBLIC_URL` wins when set, but the CLI does not set it), the path as `/functions/v1/{SUPABASE_FUNCTION_SLUG}`, and the issuer as `{origin}/auth/v1`. Supabase CLI 2.117.0 or later injects the slug locally. Without a slug the path is reconstructed from the request, which works for a function served at `/functions/v1/{name}`; a function served at the root path `/` with no slug throws `MISSING_RESOURCE_SERVER`, because there is no function segment to restore.
On Supabase Edge Functions the metadata is derived with no configuration: the public origin from the gateway's `X-Forwarded-*` headers (`SUPABASE_PUBLIC_URL` wins when set, but the CLI does not set it), the path as `/functions/v1/{SUPABASE_FUNCTION_SLUG}`, and the issuer as `{origin}/auth/v1`. Supabase CLI 2.117.0 or later injects the slug locally. Without a slug the path is reconstructed from the request, which works for a function served at `/functions/v1/{name}`; a function served at the root path `/` with no slug answers with a 500 and code `MISSING_RESOURCE_SERVER`, because there is no function segment to restore.

Anywhere else, a Next.js route handler, a Worker, a plain server, the app's origin is unrelated to the project's, so pass both URLs:

Expand All @@ -85,15 +85,17 @@ withOAuthProtectedResource({
})
```

- `resourceServer`: the public URL of this endpoint. Required off Edge Functions; `MissingResourceServerError` (`MISSING_RESOURCE_SERVER`) otherwise.
- `authorizationServer`: the OAuth issuer. Falls back to `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each with `/auth/v1`; `MissingAuthorizationServerError` (`MISSING_AUTHORIZATION_SERVER`) if neither is set. `fromSupabaseUrl(projectUrl)` builds it from a project URL. A non-Supabase OAuth 2.1 server (Clerk, WorkOS, Auth0) works too.
- `resourceServer`: the public URL of this endpoint. Required off Edge Functions; every request is answered with a 500 and code `MISSING_RESOURCE_SERVER` otherwise.
- `authorizationServer`: the OAuth issuer. Falls back to `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each with `/auth/v1`; the metadata route answers with a 500 and code `MISSING_AUTHORIZATION_SERVER` if neither is set. `fromSupabaseUrl(projectUrl)` builds it from a project URL. A non-Supabase OAuth 2.1 server (Clerk, WorkOS, Auth0) works too.

Both accept a string or `(req: Request) => string` (`UrlOption`). The full config:

| Option | Type | Default on Edge Functions | Default elsewhere |
| --------------------- | ----------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `resourceServer` | `UrlOption` | `{public origin}/functions/v1/{slug}` | none; throws `MissingResourceServerError` |
| `authorizationServer` | `UrlOption` | `{public origin}/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each `+ /auth/v1`; else throws `MissingAuthorizationServerError` |
| Option | Type | Default on Edge Functions | Default elsewhere |
| --------------------- | ----------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `resourceServer` | `UrlOption` | `{public origin}/functions/v1/{slug}` | none; `500` with code `MISSING_RESOURCE_SERVER` |
| `authorizationServer` | `UrlOption` | `{public origin}/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each `+ /auth/v1`; else `500` with code `MISSING_AUTHORIZATION_SERVER` |

Either 500 is the library's JSON error response, with the code in the `x-supabase-server-error` header and a `hint` naming the option to set; see [`docs/error-handling.md`](error-handling.md#enverror-codes). A throw from a `resourceServer` or `authorizationServer` function you supplied is yours and propagates. `errors: { detailed: false }` trims either body to `code` and `message`.

The middleware contributes `ctx.oauthProtectedResource.resourceMetadataUrl`, the resolved metadata URL, to the downstream context.

Expand Down
Loading
Loading