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
69 changes: 57 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,58 @@ withSupabase(

Adapters wrap `withSupabase` for a specific framework's middleware contract. They ship inside `@supabase/server`, so a single `npm install @supabase/server` covers the framework you're using — no separate package per adapter.

> **Adapters are a community initiative.** Every adapter in this repo originated as a community contribution — Hono and H3 included. The Supabase team reviews PRs, runs security and regression triage, and ships releases; the original contributor is the de-facto domain expert for their adapter and the first responder on framework-version bumps. Want to add a new one? See [`src/adapters/README.md`](src/adapters/README.md) for the contribution requirements (tests, types, docs, build wiring).
> **Adapters are a community-driven initiative.** They're developed, maintained, and evolved by contributors — including responding to upstream framework changes. See [`src/adapters/README.md`](src/adapters/README.md) for the contribution requirements (tests, types, docs, build wiring) if you'd like to add or help maintain one.

| Framework | Import | Framework version | Docs |
| --------- | -------------------------------- | ----------------- | ---------------------------------------------- |
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
| Framework | Import | Framework version | Docs |
| --------- | ---------------------------------- | ----------------- | -------------------------------------------------- |
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |

See the per-adapter docs above for setup, per-route auth, CORS, error handling, and other patterns.

### Elysia

```ts
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
// Protected — plugin resolves supabaseContext before handlers run
.use(withSupabase({ auth: 'user' }))
.get('/games', async ({ supabaseContext }) => {
const { data: myGames } = await supabaseContext.supabase
.from('favorite_games')
.select()
return myGames
})
// Public — no plugin means no auth
.get('/health', () => ({ status: 'ok' }))

app.listen(3000)
```

For per-route auth, use scoped groups:

```ts
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
}),
)

app.listen(3000)
```

The adapter does not handle CORS — use `@elysiajs/cors` for that.

## Primitives

For when you need more control than `withSupabase` provides — multiple routes with different auth, custom response headers, or building your own wrapper.
Expand Down Expand Up @@ -407,20 +450,21 @@ For other environments, pass overrides via the `env` config option or `resolveEn
| **Deno / Bun** | Works out of the box via `export default { fetch }`. |
| **Node.js** | Use a [framework adapter](#framework-adapters) or [core primitives](#primitives) with your framework of choice. |

Using a framework? See [Framework Adapters](#framework-adapters) for Hono and H3 / Nuxt, or [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) for Next.js / SvelteKit / Remix (compose with [`@supabase/ssr`](https://github.com/supabase/ssr)).
Using a framework? See [Framework Adapters](#framework-adapters) for Hono, H3 / Nuxt, and Elysia, or [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) for Next.js / SvelteKit / Remix (compose with [`@supabase/ssr`](https://github.com/supabase/ssr)).

### Does this replace `@supabase/ssr`?

No. `@supabase/ssr` handles cookie-based session management for frameworks like Next.js and SvelteKit. `@supabase/server` handles stateless, header-based auth for Edge Functions, Workers, and other backend runtimes. The composable primitives already work in SSR environments but require more setup — see [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) for the Next.js example. The two packages coexist and are not replacements for each other. Deeper integration with `@supabase/ssr` is on the roadmap.

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

## Documentation

Expand All @@ -431,6 +475,7 @@ 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
140 changes: 140 additions & 0 deletions docs/adapters/elysia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Elysia Adapter

## Setup

Install Elysia as a peer dependency:

```bash
pnpm add elysia
```

The adapter exports its own `withSupabase` that returns an Elysia plugin instead of a fetch handler.

## Basic app with auth

```ts
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
.use(withSupabase({ auth: 'user' }))
.get('/todos', async ({ supabaseContext }) => {
const { data } = await supabaseContext.supabase.from('todos').select()
return data
})

app.listen(3000)
```

The context is available as `supabaseContext` in your route handlers and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, and `authMode`.

## Per-route auth

Apply different auth modes to different routes by using the plugin on scoped route groups:

```ts
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
// Public route — no auth
.get('/health', () => ({ status: 'ok' }))
// User-authenticated routes
.group('/api', (app) =>
app
.use(withSupabase({ auth: 'user' }))
.get('/todos', async ({ supabaseContext }) => {
const { data } = await supabaseContext.supabase.from('todos').select()
return data
}),
)
// Secret-key-protected admin routes
.group('/admin', (app) =>
app
.use(withSupabase({ auth: 'secret' }))
.post('/sync', async ({ supabaseContext }) => {
const { data } = await supabaseContext.supabaseAdmin
.from('audit_log')
.insert({ action: 'sync' })
return data
}),
)

app.listen(3000)
```

## Skip behavior

If a previous plugin already resolved `supabaseContext`, subsequent `withSupabase` calls skip auth. This allows chaining plugins without redundant work.

**Important:** The plugin calls `.as('scoped')` so its `resolve` hook propagates one level up to the parent app — routes registered after `.use(withSupabase(...))` will see `supabaseContext`. The skip-if-set pattern cannot make a route stricter than an already-resolved context.

For routes that need different auth than the rest of the app, use scoped `.group()` with `.use(withSupabase(...))` without an app-wide plugin (see the "Per-route auth" section above).

## CORS

The Elysia adapter does not handle CORS — the `cors` option is excluded from its config type. Use Elysia's CORS plugin:

```ts
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
.use(cors())
.use(withSupabase({ auth: 'user' }))
.get('/todos', async ({ supabaseContext }) => {
const { data } = await supabaseContext.supabase.from('todos').select()
return data
})

app.listen(3000)
```

## Error handling

When auth fails, the plugin throws a `SupabaseError`. The HTTP status is on `.status` directly, and the original `AuthError` is available as the typed `.cause`. Discriminate in `onError` via `code === 'SupabaseError'`:

```ts
import { Elysia } from 'elysia'
import { withSupabase } from '@supabase/server/adapters/elysia'

const app = new Elysia()
.use(withSupabase({ auth: 'user' }))
.onError(({ code, error, status }) => {
if (code !== 'SupabaseError') return
return status(error.status as 401, {
error: error.message,
code: error.cause.code,
})
})
.get('/todos', async ({ supabaseContext }) => {
const { data } = await supabaseContext.supabase.from('todos').select()
return data
})

app.listen(3000)
```

Without a custom `onError`, Elysia uses the `status` property on the thrown `SupabaseError` to set the response status automatically (401 for auth failures, 500 for internal errors).

## Environment overrides

Pass `env` to override auto-detected environment variables, same as the main wrapper:

```ts
app.use(withSupabase({ auth: 'user', env: { url: 'http://localhost:54321' } }))
```

## Supabase client options

Forward options to the underlying `createClient()` calls:

```ts
app.use(
withSupabase({
auth: 'user',
supabaseOptions: { db: { schema: 'api' } },
}),
)
```
32 changes: 32 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,38 @@ Defaults to `auth: 'user'` when config is omitted.

---

## @supabase/server/adapters/h3

### withSupabase (H3)

```ts
function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware
```

H3 middleware. Sets `event.context.supabaseContext` on the H3 event. Throws `HTTPError` on auth failure with `cause: AuthError`.

Skips if `event.context.supabaseContext` is already set (enables chained middleware).

Defaults to `auth: 'user'` when config is omitted.

---

## @supabase/server/adapters/elysia

### withSupabase (Elysia)

```ts
function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Elysia
```

Elysia plugin that resolves `supabaseContext` into the request context. Throws an error on auth failure with `cause: AuthError`.

Skips if `supabaseContext` is already resolved by a prior plugin.

Defaults to `auth: 'user'` when config is omitted.

---

## Types

### AuthMode
Expand Down
15 changes: 5 additions & 10 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@
"exports": {
".": "./src/index.ts",
"./core": "./src/core/index.ts",
"./adapters/hono": "./src/adapters/hono/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"]
}
}
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
"import": "./dist/adapters/h3/index.mjs",
"require": "./dist/adapters/h3/index.cjs"
},
"./adapters/elysia": {
"types": "./dist/adapters/elysia/index.d.mts",
"import": "./dist/adapters/elysia/index.mjs",
"require": "./dist/adapters/elysia/index.cjs"
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -72,21 +77,26 @@
"peerDependencies": {
"@supabase/supabase-js": "^2.0.0",
"h3": "^2.0.0",
"hono": "^4.0.0"
"hono": "^4.0.0",
"elysia": "^1.4.0"
},
"peerDependenciesMeta": {
"h3": {
"optional": true
},
"hono": {
"optional": true
},
"elysia": {
"optional": true
}
},
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@supabase/supabase-js": "^2.105.4",
"eslint": "^10.0.2",
"elysia": "^1.4.0",
"h3": "2.0.1-rc.20",
"hono": "^4.12.5",
"prettier": "3.8.1",
Expand Down
Loading