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
59 changes: 52 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,48 @@ export default withSupabase({ allow: 'user' })

The adapter does not handle CORS — use H3's CORS utilities for that.

### 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({ allow: '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({ allow: '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 @@ -428,19 +470,21 @@ For other environments, pass overrides via the `env` config option or `resolveEn

- **Supabase Edge Functions** — environment variables are auto-injected. Zero config.
- **Deno / Bun** — works out of the box with the `export default { fetch }` pattern.
- **Node.js** — use the [Hono adapter](#hono), [H3 adapter](#h3--nuxt), or [core primitives](#primitives) with your framework of choice.
- **Node.js** — use the [Hono adapter](#hono), [H3 adapter](#h3--nuxt), [Elysia adapter](#elysia), or [core primitives](#primitives) with your framework of choice.
- **Cloudflare Workers** — enable `nodejs_compat` in `wrangler.toml` or pass env overrides via the `env` config option.
- **Nuxt** — use the [H3 adapter](#h3--nuxt) directly as a server middleware.
- **Elysia** — use the [Elysia adapter](#elysia) as a plugin.
- **Next.js / SvelteKit / Remix** — use core primitives to build a cookie-based auth adapter. See [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md).

## 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 @@ -449,6 +493,7 @@ For other environments, pass overrides via the `env` config option or `resolveEn
| How do I create a basic endpoint? | [`docs/getting-started.md`](docs/getting-started.md) |
| What auth modes are available? Array syntax? Named keys? | [`docs/auth-modes.md`](docs/auth-modes.md) |
| How do I use this with Hono? | [`docs/hono-adapter.md`](docs/hono-adapter.md) |
| How do I use this with Elysia? | [`docs/elysia-adapter.md`](docs/elysia-adapter.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
32 changes: 32 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,38 @@ Defaults to `allow: '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 `allow: '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 `allow: 'user'` when config is omitted.

---

## Types

### Allow
Expand Down
141 changes: 141 additions & 0 deletions docs/elysia-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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({ allow: '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`, `claims`, and `authType`.

## 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({ allow: '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({ allow: '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({ allow: '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 an error with the correct HTTP status code set. The original `AuthError` is available via `error.cause` in an `onError` handler:

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

const app = new Elysia()
.use(withSupabase({ allow: 'user' }))
.onError(({ code, error, status }) => {
if (code !== 'SupabaseAuthError') return
const cause = error.cause as { code?: string; status?: number } | undefined
return status((cause?.status as 401) ?? 500, {
error: error.message,
code: 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 error 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({ allow: 'user', env: { url: 'http://localhost:54321' } }))
```

## Supabase client options

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

```ts
app.use(
withSupabase({
allow: 'user',
supabaseOptions: { db: { schema: 'api' } },
}),
)
```
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 @@ -76,21 +81,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.98.0",
"eslint": "^10.0.2",
"elysia": "^1.0.0",
"h3": "2.0.1-rc.20",
"hono": "^4.12.5",
"prettier": "3.8.1",
Expand Down
Loading
Loading