Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
785577e
feat(gates): add composable gate primitives for fetch handlers
johnstonmatt May 2, 2026
44365a9
feat(gates): add x402 paywall gate
johnstonmatt May 2, 2026
d240d9b
feat(gates): add Cloudflare Turnstile gate
johnstonmatt May 2, 2026
eac3548
feat(gates): add Cloudflare Access gate
johnstonmatt May 2, 2026
4318b35
feat(gates): add webhook signature verification gate
johnstonmatt May 2, 2026
9b784b8
feat(gates): add fixed-window rate-limit gate
johnstonmatt May 2, 2026
fe612da
feat(gates): add provider-agnostic feature-flag gate
johnstonmatt May 2, 2026
5e33c35
chore(gates): register flag, rate-limit, and webhook subpath exports
johnstonmatt May 2, 2026
ac38951
refactor(gates)!: replace chain composer with direct nesting
johnstonmatt May 2, 2026
ecd985d
refactor(gates)!: replace pluggable stores with Supabase RPC persistence
johnstonmatt May 2, 2026
7dfb762
Merge branch 'main' of https://github.com/supabase/server into FUNC-5…
johnstonmatt May 2, 2026
b579ea5
refactor(gates)!: replace GateResult union with direct Response or ke…
johnstonmatt May 7, 2026
6508c4a
refactor(gates)!: infer nested ctx without explicit Base annotations
johnstonmatt May 7, 2026
20e2b89
test(gates): add regression tests and runtime check for missing gate key
johnstonmatt May 7, 2026
b65339e
Merge branch 'main' of https://github.com/supabase/server into FUNC-5…
johnstonmatt May 7, 2026
0683053
test(gates): rename authType to authMode in nested ctx test
johnstonmatt May 7, 2026
359f757
refactor(gates): annotate built-in gates with explicit GateFactory types
johnstonmatt May 7, 2026
e62e830
refactor(gates)!: rename GateFactory type to Gate
johnstonmatt May 7, 2026
4b6827f
feat(gates/webhook): add built-in GitHub provider
johnstonmatt May 8, 2026
69eda65
docs(gates): reframe as portable extensibility layer
johnstonmatt May 8, 2026
b5b4851
docs(gates): document composition rules for stacking gates
johnstonmatt May 8, 2026
64af25e
docs(gates): lead README with withSupabase analogy
johnstonmatt May 8, 2026
677efea
docs(gates): tighten README prose and drop internal asides
johnstonmatt May 8, 2026
8ff9455
Merge branch 'main' of https://github.com/supabase/server into FUNC-5…
johnstonmatt May 11, 2026
cbffc47
refactor(gates): consolidate built-ins to feature-flag worked example
johnstonmatt May 15, 2026
402965c
Merge branch 'main' of https://github.com/supabase/server into FUNC-5…
johnstonmatt May 20, 2026
2ecbdf8
feat(adapters): add two-arg withSupabase form for gate composition
johnstonmatt May 20, 2026
78aaa21
test(adapters): add contract test for two-arg withSupabase delegation
johnstonmatt May 20, 2026
b25723c
fix: throw helpful TypeError when two-arg form receives framework con…
johnstonmatt May 22, 2026
3c5bf4e
feat(adapters): accept framework context directly in two-arg withSupa…
johnstonmatt May 22, 2026
b6f5fbb
refactor(adapters): extract two-arg withSupabase into defineAdapter h…
johnstonmatt May 22, 2026
23059f6
Merge branch 'main' of https://github.com/supabase/server into FUNC-5…
johnstonmatt May 22, 2026
e614521
feat(adapters): add onAuthError hook and skip-if-set context to two-a…
johnstonmatt May 22, 2026
d94a774
docs(adapters): document two-arg form behavior and defineAdapter pattern
johnstonmatt May 23, 2026
5a8e582
feat(core): expose defineAdapter via @supabase/server/core/adapters s…
johnstonmatt May 23, 2026
6bde967
feat(adapters): add defineAdapter for uniform framework integrations
johnstonmatt May 26, 2026
5cef8cc
refactor(gates): rename FeatureFlagState to FeatureFlagContribution a…
johnstonmatt May 26, 2026
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
15 changes: 6 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,22 @@

## [1.1.0](https://github.com/supabase/server/compare/server-v1.0.0...server-v1.1.0) (2026-05-19)


### Features

* add Elysia adapter ([#46](https://github.com/supabase/server/issues/46)) ([148169e](https://github.com/supabase/server/commit/148169e5f7737ea50049f3649056f5a44a266a1f))
* **env:** add support for JWKS discovery endpoints ([#53](https://github.com/supabase/server/issues/53)) ([45d677a](https://github.com/supabase/server/commit/45d677ae6539cfa58e0c339960f53e9a7ca90e7d))

- add Elysia adapter ([#46](https://github.com/supabase/server/issues/46)) ([148169e](https://github.com/supabase/server/commit/148169e5f7737ea50049f3649056f5a44a266a1f))
- **env:** add support for JWKS discovery endpoints ([#53](https://github.com/supabase/server/issues/53)) ([45d677a](https://github.com/supabase/server/commit/45d677ae6539cfa58e0c339960f53e9a7ca90e7d))

### Bug Fixes

* **auth:** skip user mode when token has sb_ prefix ([#67](https://github.com/supabase/server/issues/67)) ([b193216](https://github.com/supabase/server/commit/b1932169e28163040b9b22db73b0f84739d9bb8b))
* **ci:** update node packages ([#57](https://github.com/supabase/server/issues/57)) ([f275907](https://github.com/supabase/server/commit/f2759071fd84932e15ebd48f21c04ab311bd5237))
* **jsr:** resolve slow-type errors in elysia and h3 adapters ([#69](https://github.com/supabase/server/issues/69)) ([7c56b13](https://github.com/supabase/server/commit/7c56b132985bd04673108dab7251b1939326d18e))
- **auth:** skip user mode when token has sb\_ prefix ([#67](https://github.com/supabase/server/issues/67)) ([b193216](https://github.com/supabase/server/commit/b1932169e28163040b9b22db73b0f84739d9bb8b))
- **ci:** update node packages ([#57](https://github.com/supabase/server/issues/57)) ([f275907](https://github.com/supabase/server/commit/f2759071fd84932e15ebd48f21c04ab311bd5237))
- **jsr:** resolve slow-type errors in elysia and h3 adapters ([#69](https://github.com/supabase/server/issues/69)) ([7c56b13](https://github.com/supabase/server/commit/7c56b132985bd04673108dab7251b1939326d18e))

## [1.0.0](https://github.com/supabase/server/compare/server-v0.2.0...server-v1.0.0) (2026-05-06)


### Miscellaneous Chores

* release 1.0.0 ([#50](https://github.com/supabase/server/issues/50)) ([67de77f](https://github.com/supabase/server/commit/67de77f00b7ebbf4e1de973489703959c7e3a838))
- release 1.0.0 ([#50](https://github.com/supabase/server/issues/50)) ([67de77f](https://github.com/supabase/server/commit/67de77f00b7ebbf4e1de973489703959c7e3a838))

## [0.2.0](https://github.com/supabase/server/compare/server-v0.1.4...server-v0.2.0) (2026-04-24)

Expand Down
29 changes: 10 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,20 +301,13 @@ For per-route auth, use scoped groups:
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
.get('/health', () => ({ status: 'ok' }))
.group('/api', (app) =>
app
.use(withSupabase({ auth: 'user' }))
.get('/profile', async ({ supabaseContext }) => {
return supabaseContext.userClaims
}),
)
const app = new H3()
app.use(withSupabase({ auth: 'user' }))

app.listen(3000)
export default { fetch: app.fetch }
```

The adapter does not handle CORS — use `@elysiajs/cors` for that.
See [docs/adapters/h3.md](docs/adapters/h3.md) for per-route auth, Nuxt server-middleware patterns, CORS, and more.

## Primitives

Expand Down Expand Up @@ -458,13 +451,12 @@ 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) |
| 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) |

## Documentation

Expand All @@ -475,7 +467,6 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
| Which framework adapters exist? How do I contribute one? | [`src/adapters/README.md`](src/adapters/README.md) |
| How do I use this with Hono? | [`docs/adapters/hono.md`](docs/adapters/hono.md) |
| How do I use this with H3 / Nuxt? | [`docs/adapters/h3.md`](docs/adapters/h3.md) |
| How do I use this with Elysia? | [`docs/adapters/elysia.md`](docs/adapters/elysia.md) |
| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) |
| 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) |
Expand Down
34 changes: 33 additions & 1 deletion docs/adapters/elysia.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ Install Elysia as a peer dependency:
pnpm add elysia
```

The adapter exports its own `withSupabase` that returns an Elysia plugin instead of a fetch handler.
The adapter exports `withSupabase` with two call shapes:

- **One arg** — `withSupabase(config)` — returns an Elysia plugin. Everything in this document describes this form.
- **Two args** — `withSupabase(config, handler)` — the base `withSupabase` from `@supabase/server`, re-exported here for ergonomics. Returns a Web Fetch handler. Use it when you want to compose with [gates](../../src/core/gates/README.md). See the "Composing with gates" section at the bottom.

## Basic app with auth

Expand Down Expand Up @@ -138,3 +141,32 @@ app.use(
}),
)
```

## Composing with gates

For routes that compose with a [gate](../../src/core/gates/README.md), call `withSupabase` with two args. That form returns a dual-mode handler — it accepts either an Elysia route context (when mounted on a route) or a plain `Request` (Web Fetch) — and extracts the underlying Request automatically. Mount directly with `.all(path, withSupabase(...))`:

```ts
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'
import { withFeatureFlag } from '@supabase/server/gates/feature-flag'

new Elysia()
.all(
'/beta',
withSupabase(
{ auth: 'user' },
withFeatureFlag(
{ name: 'beta', evaluate: (req) => req.headers.has('x-beta') },
async (_req, ctx) =>
Response.json({
user: ctx.userClaims?.id,
flag: ctx.featureFlag.name,
}),
),
),
)
.listen(3000)
```

Routes that don't need a gate continue to use the one-arg plugin form documented above. The two coexist in one app; each route picks the form that fits.
31 changes: 30 additions & 1 deletion docs/adapters/h3.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ Install H3 as a peer dependency:
pnpm add h3
```

The adapter exports its own `withSupabase` that returns H3 middleware instead of a fetch handler. Works with standalone H3 servers and Nuxt server routes (which run on H3 under the hood).
The adapter exports `withSupabase` with two call shapes:

- **One arg** — `withSupabase(config)` — returns H3 middleware. Everything in this document describes this form. Works with standalone H3 servers and Nuxt server routes (which run on H3 under the hood).
- **Two args** — `withSupabase(config, handler)` — the base `withSupabase` from `@supabase/server`, re-exported here for ergonomics. Returns a Web Fetch handler. Use it when you want to compose with [gates](../../src/core/gates/README.md). See the "Composing with gates" section at the bottom.

## Typing `event.context.supabaseContext`

Expand Down Expand Up @@ -192,3 +195,29 @@ app.use(
}),
)
```

## Composing with gates

For routes that compose with a [gate](../../src/core/gates/README.md), call `withSupabase` with two args. That form returns a dual-mode handler — it accepts either an `H3Event` (when mounted on a route) or a plain `Request` (Web Fetch) — and extracts the underlying Request automatically. Mount directly with `app.all(path, withSupabase(...))`:

```ts
import { H3 } from 'h3'
import { withSupabase } from '@supabase/server/adapters/h3'
import { withFeatureFlag } from '@supabase/server/gates/feature-flag'

const app = new H3()

app.all(
'/beta',
withSupabase(
{ auth: 'user' },
withFeatureFlag(
{ name: 'beta', evaluate: (req) => req.headers.has('x-beta') },
async (_req, ctx) =>
Response.json({ user: ctx.userClaims?.id, flag: ctx.featureFlag.name }),
),
),
)
```

Routes that don't need a gate continue to use the one-arg middleware form documented above. The two coexist in one app; each route picks the form that fits.
31 changes: 30 additions & 1 deletion docs/adapters/hono.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ Install Hono as a peer dependency:
pnpm add hono
```

The adapter exports its own `withSupabase` that returns Hono middleware instead of a fetch handler.
The adapter exports `withSupabase` with two call shapes:

- **One arg** — `withSupabase(config)` — returns Hono middleware. Everything in this document describes this form.
- **Two args** — `withSupabase(config, handler)` — the base `withSupabase` from `@supabase/server`, re-exported here for ergonomics. Returns a Web Fetch handler. Use it when you want to compose with [gates](../../src/core/gates/README.md). See the "Composing with gates" section at the bottom.

## Basic app with auth

Expand Down Expand Up @@ -172,3 +175,29 @@ app.use(
}),
)
```

## Composing with gates

For routes that compose with a [gate](../../src/core/gates/README.md), call `withSupabase` with two args. That form returns a dual-mode handler — it accepts either a Hono `Context` (when mounted on a route) or a plain `Request` (Web Fetch) — and extracts the underlying Request automatically. Mount directly with `app.all(path, withSupabase(...))`:

```ts
import { Hono } from 'hono'
import { withSupabase } from '@supabase/server/adapters/hono'
import { withFeatureFlag } from '@supabase/server/gates/feature-flag'

const app = new Hono()

app.all(
'/beta',
withSupabase(
{ auth: 'user' },
withFeatureFlag(
{ name: 'beta', evaluate: (req) => req.headers.has('x-beta') },
async (_req, ctx) =>
Response.json({ user: ctx.userClaims?.id, flag: ctx.featureFlag.name }),
),
),
)
```

Routes that don't need a gate continue to use the one-arg middleware form documented above. The two coexist in one app; each route picks the form that fits.
12 changes: 3 additions & 9 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@
"exports": {
".": "./src/index.ts",
"./core": "./src/core/index.ts",
"./core/adapters": "./src/core/adapters/index.ts",
"./adapters/hono": "./src/adapters/hono/index.ts",
"./adapters/h3": "./src/adapters/h3/index.ts",
"./adapters/elysia": "./src/adapters/elysia/index.ts"
},
"publish": {
"include": [
"src/**/*.ts",
"README.md",
"LICENSE"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.spec.ts"
]
"include": ["src/**/*.ts", "README.md", "LICENSE"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
"import": "./dist/core/index.mjs",
"require": "./dist/core/index.cjs"
},
"./core/adapters": {
"types": "./dist/core/adapters/index.d.mts",
"import": "./dist/core/adapters/index.mjs",
"require": "./dist/core/adapters/index.cjs"
},
"./adapters/hono": {
"types": "./dist/adapters/hono/index.d.mts",
"import": "./dist/adapters/hono/index.mjs",
Expand Down
63 changes: 61 additions & 2 deletions src/adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Before you start, **read [`CONTRIBUTING.md`](../../CONTRIBUTING.md) and agree wi
- **Tests for every auth mode.** Cover `'user'`, `'publishable'`, `'secret'`, `'none'`, the array form, and the failure paths (missing token, invalid JWT, missing apikey). The Hono adapter's [`hono/middleware.test.ts`](hono/middleware.test.ts) is the canonical reference — your test file should look structurally similar.
- **Strict TypeScript.** No `any`, no `// @ts-ignore`. Public types must be exported from the adapter's `index.ts` so consumers can extend them.
- **No new runtime dependencies** beyond the framework you're adapting. The framework itself goes in `peerDependencies` (and `peerDependenciesMeta` if optional). Don't pull in a wrapper, polyfill, or utility lib just to make the adapter shorter.
- **Match the existing adapter shape.** Export `withSupabase(config, handler)` returning the framework's native middleware/handler type. Use `verifyAuth`, `createContextClient`, and `createAdminClient` from `@supabase/server/core` — never re-implement auth or env handling inside an adapter.
- **Match the existing adapter shape.** Export `withSupabase` with two call forms — `withSupabase(config)` returning the framework's native middleware/plugin, and `withSupabase(config, handler)` returning a dual-mode route handler built via [`defineAdapter`](../core/adapters/define-adapter.ts) (see [Designing an adapter](#designing-an-adapter) below). Use `verifyAuth`, `createContextClient`, and `createAdminClient` from `@supabase/server/core` — never re-implement auth or env handling inside an adapter.
- **Wire up the build outputs.** Add the adapter entry to `package.json#exports`, `jsr.json` (if applicable), and `tsdown.config.ts#entry` so it ships in the published artifact.
- **Docs are required.** Add `docs/adapters/<name>.md` mirroring the structure of [`docs/adapters/hono.md`](../../docs/adapters/hono.md) — at minimum: setup, basic example, per-route auth, CORS note.
- **Update both adapter tables.** Add a row to the table in this `src/adapters/README.md` _and_ the mirror table in the top-level [`README.md`](../../README.md). Keep the framework-version column accurate against `package.json#peerDependencies`. PRs that touch an existing adapter must update the version column if the peer-dep range changed.
Expand All @@ -36,4 +36,63 @@ The Supabase team will review the PR against these requirements. Once merged, th

## Designing an adapter

The existing adapters at [`hono/middleware.ts`](hono/middleware.ts), [`h3/middleware.ts`](h3/middleware.ts), and [`elysia/plugin.ts`](elysia/plugin.ts) (siblings of this README) are the canonical templates. The shape every adapter exposes is `withSupabase(config, handler)` returning a framework-native middleware. Keep all auth logic in `@supabase/server/core` — adapters should only translate request/response shapes between the framework and the core primitives.
Every adapter has two call forms. They share a name (`withSupabase`) but solve different problems and are implemented differently:

### One-arg form — bespoke per framework

`withSupabase(config)` returns framework-native middleware/plugin (e.g. Hono `MiddlewareHandler`, H3 `Middleware`, Elysia plugin). This is the form users apply with `app.use(...)`. Each framework has its own:

- middleware/plugin construction (`createMiddleware`, `defineMiddleware`, `new Elysia().resolve(...)`),
- context-population idiom (`c.set('supabaseContext', ctx)`, `event.context.supabaseContext = ctx`, `.resolve(() => ({ supabaseContext: ctx }))`),
- error-throw shape (`HTTPException`, `HTTPError`, a registered custom error class for Elysia).

There's no useful shared abstraction here — the divergence is structural. Mirror the existing adapter that's closest to your framework's idiom.

Common contract every one-arg implementation must uphold:

- **Skip if a previous middleware already set `supabaseContext`.** Enables route-level overrides via scoped/grouped middleware. See [`hono/middleware.ts`](hono/middleware.ts) for the canonical check.
- **Throw a framework-native error on auth failure**, not a returned Response. The error must carry the original `AuthError` as `.cause` so users can discriminate on `cause.code` / `cause.status` in their `onError` hook.
- **Exclude `cors` from the config type** (`Omit<WithSupabaseConfig, 'cors'>`). CORS belongs to the framework's CORS middleware/plugin, not to the adapter.

### Two-arg form — use `defineAdapter`

`withSupabase(config, handler)` returns a dual-mode route handler that accepts either a `Request` (Web Fetch use) or the framework's native route input (`Context`, `H3Event`, Elysia args), extracts the underlying Request, and runs base `withSupabase` against it. Mountable directly via `app.all(path, withSupabase(config, handler))`.

Don't hand-roll this — [`defineAdapter`](../core/adapters/define-adapter.ts) (exported publicly as `@supabase/server/core/adapters`) encapsulates the entire dual-mode contract, including:

- Request extraction from the framework's native input.
- `cors: false` forced on the base call (the framework owns CORS).
- Optional skip-if-set: when an upstream middleware already populated `supabaseContext`, the inner handler runs with that context instead of re-verifying.
- Optional `throwAuthError`: surfaces auth failures through the framework's error pipeline, matching the one-arg form's behavior.

Wire it up at the top of your adapter file:

```ts
// In-tree (bundled adapters in this repo):
import { defineAdapter } from '../../core/adapters/index.js'

// Third-party adapter published as its own npm package:
// import { defineAdapter } from '@supabase/server/core/adapters'

const adapterWithSupabase = defineAdapter<MyFrameworkContext>({
name: 'my-framework',
extractRequest: (ctx) => ctx.request, // required
getExistingContext: (ctx) => ctx.var?.supabaseContext, // optional: skip-if-set
throwAuthError: (error) => {
throw new MyFrameworkError(error) // optional: framework-native errors
},
})
```

Then in your `withSupabase` implementation, the two-arg branch is one line:

```ts
if (handler) return adapterWithSupabase(config!, handler)
```

The two-arg overload's config type is `Omit<WithSupabaseConfig, 'cors' | 'onAuthError'>` — `defineAdapter` controls both internally. See [`hono/middleware.ts`](hono/middleware.ts) for the canonical pattern.

### Shared rules across both forms

- Keep all auth logic in `@supabase/server/core` — adapters only translate request/response shapes between the framework and the core primitives.
- The one-arg and two-arg forms must agree on behavior: same skip semantics, same framework-native error on auth failure, same CORS exclusion. `defineAdapter`'s hooks exist specifically to keep them in sync.
Loading
Loading