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
64 changes: 56 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,45 @@ export default {
}
```

## Postgres (RLS-scoped queries)

When PostgREST isn't the right tool — joins, CTEs, window functions — `withPostgresClient` puts a direct Postgres connection on `ctx.postgres`, scoped to the caller by RLS:

```ts
import { withSupabase } from '@supabase/server'
import { withPostgresClient } from '@supabase/server/middleware/postgres'

export default {
fetch: withSupabase(
{ auth: 'user', middleware: [withPostgresClient()] },
async (_req, ctx) => {
// No WHERE clause — RLS scopes the rows to the caller.
const notes = await ctx.postgres.query('select id, body from notes')
return Response.json(notes)
},
),
}
```

Each query runs in its own transaction that injects the caller's claims and drops to their role, exactly like PostgREST — so `auth.uid()` resolves and your policies enforce. Only `authenticated` and `anon` are assumed; a token naming any other role (including `service_role`, and custom roles) is refused with `code: 'UNSUPPORTED_ROLE'` rather than silently downgraded to `anon`.

When a handler legitimately needs to cross user boundaries, `withPostgresAdminClient` is the explicit opt-out — it contributes `ctx.postgresAdmin`, which bypasses RLS and needs no caller identity, so it works under `auth: 'secret'` and `auth: 'none'` too:

```ts
import { withPostgresAdminClient } from '@supabase/server/middleware/postgres-admin'

withSupabase(
{ auth: 'secret', middleware: [withPostgresAdminClient()] },
handler,
)
```

The pair mirrors `ctx.supabase` / `ctx.supabaseAdmin`, and they share one connection pool. Keeping them as two middleware is deliberate: bypassing RLS stays visible at the composition site, so you can grep for every handler that can do it.

Needs `pg` installed (optional peer dependency) and a raw TCP socket: Node, Deno, Bun, and the Supabase Edge runtime — **not** Workers-style isolates. Reads `SUPABASE_DB_URL` by default. Remember that `authenticated` also needs table grants, not just policies.

See [`docs/postgres.md`](docs/postgres.md) for standalone composition with `withClaims`, the grants requirement, and current limits.

## Environment Variables

Automatically available in Supabase Edge Functions:
Expand All @@ -456,6 +495,7 @@ Also supported (for local dev, self-hosted, or other runtimes):
| `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key |
| `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key |
| `SUPABASE_JWKS_URL` | `https://...` | Remote JWKS endpoint (used when `SUPABASE_JWKS` is unset) |
| `SUPABASE_DB_URL` | `postgresql://...` | Postgres connection string, read by `withPostgresClient` |

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

Expand All @@ -481,14 +521,21 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like

## Exports

| Export | What's in it |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `@supabase/server` | `withSupabase`, `createSupabaseContext` |
| `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` |
| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) |
| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) |
| Export | What's in it |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `@supabase/server` | `withSupabase`, `createSupabaseContext` |
| `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` |
| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) |
| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) |
| `@supabase/server/middleware/client` | `withSupabaseClient` (RLS-scoped `ctx.supabase` client) |
| `@supabase/server/middleware/admin-client` | `withSupabaseAdminClient` (`ctx.supabaseAdmin`, bypasses RLS) |
| `@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/peer/supabase-js` | Re-exported `supabase-js` types (`SupabaseClient`, `PostgrestError`, …) |

## Documentation

Expand All @@ -505,6 +552,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) |
| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) |
| How do I get typed database queries? | [`docs/typescript-generics.md`](docs/typescript-generics.md) |
| How do I run raw SQL scoped to the caller by RLS? | [`docs/postgres.md`](docs/postgres.md) |
| How do I use this with `@supabase/ssr` (Next.js, SvelteKit, Remix)? | [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) |
| What's the complete API surface? | [`docs/api-reference.md`](docs/api-reference.md) |

Expand Down
117 changes: 106 additions & 11 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,100 @@ Defaults to `auth: 'user'` when config is omitted.

---

## @supabase/server/middleware/postgres

### withPostgresClient

```ts
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 `{ message, code: 'ENV_ERROR' }` 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`](postgres.md).

### PostgresApi

```ts
interface PostgresApi {
query<T = Record<string, unknown>>(
text: string,
params?: unknown[],
): Promise<T[]>
}
```

The value at `ctx.postgres`. `query` returns the result rows directly (not a `pg` `Result`). Use `params` for placeholders (`$1`, `$2`, …) rather than interpolating values into `text`.

### WithPostgresClientConfig

```ts
interface WithPostgresClientConfig {
connectionString?: string
}
```

Defaults to the `SUPABASE_DB_URL` environment variable. Pools are created lazily, one per connection string per process.

### RequestClaims

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

```ts
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 `{ message, code: 'ENV_ERROR' }` 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

```ts
interface WithPostgresAdminClientConfig {
connectionString?: string
}
```

Defaults to the `SUPABASE_DB_URL` environment variable.

---

## Types

### AuthMode
Expand Down Expand Up @@ -353,17 +447,18 @@ class AuthError extends Error {

## 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 |
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error |
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched, or JWT failed verification |
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth |
| 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 |
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error |
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched, or JWT failed verification |
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth |
| `UnsupportedRoleError` | `'UNSUPPORTED_ROLE'` | — | `withPostgresClient` will not assume the caller's `role` claim |

---

Expand Down
Loading
Loading