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
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,11 @@ withSupabase(

Adapters wrap `withSupabase` for a specific framework's middleware contract. **All adapters are community-maintained** — both Hono and H3 originated as community contributions. They live in this repo and ship with the core package, so a single `npm install @supabase/server` covers the framework you're using. See [`src/adapters/README.md`](src/adapters/README.md) for the maintenance model and the requirements for contributing a new adapter.

| 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) |
| NestJS | `@supabase/server/adapters/nestjs` | `^10.0.0 \|\| ^11.0.0` | [docs/adapters/nestjs.md](docs/adapters/nestjs.md) |

### Hono

Expand Down Expand Up @@ -297,6 +298,25 @@ export default { fetch: app.fetch }

See [docs/adapters/h3.md](docs/adapters/h3.md) for per-route auth, Nuxt server-middleware patterns, CORS, and more.

### NestJS

```ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
import type { SupabaseContext } from '@supabase/server'

@Controller('games')
@UseGuards(withSupabase({ auth: 'user' }))
export class GamesController {
@Get()
list(@SupabaseCtx() ctx: SupabaseContext) {
return ctx.supabase.from('favorite_games').select()
}
}
```

See [docs/adapters/nestjs.md](docs/adapters/nestjs.md) for per-route auth, exception filters, CORS, and more.

## 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 @@ -433,12 +453,13 @@ 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) |
| 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/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) |

## Documentation

Expand All @@ -449,6 +470,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 NestJS? | [`docs/adapters/nestjs.md`](docs/adapters/nestjs.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
204 changes: 204 additions & 0 deletions docs/adapters/nestjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# NestJS Adapter

## Setup

Install NestJS as a peer dependency:

```bash
pnpm add @nestjs/common @nestjs/core
```

The adapter exports `withSupabase` (a guard factory) and `SupabaseCtx` (a param decorator). Together they replace the `c.var.supabaseContext` / `event.context.supabaseContext` patterns from the Hono and H3 adapters.

`withSupabase(config)` returns a `CanActivate` guard class. The guard reads the underlying request (Express or Fastify), verifies credentials with `@supabase/server/core`, and attaches the resulting `SupabaseContext` to `request.supabaseContext`. From any handler you can pull it out with `@SupabaseCtx()`.

## Basic controller with auth

```ts
// games.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
import type { SupabaseContext } from '@supabase/server'

@Controller('games')
@UseGuards(withSupabase({ auth: 'user' }))
export class GamesController {
@Get()
async list(@SupabaseCtx() ctx: SupabaseContext) {
const { data } = await ctx.supabase.from('favorite_games').select()
return data
}

@Get('me')
me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
return user
}
}
```

`@SupabaseCtx()` returns the entire `SupabaseContext` (`supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, `authMode`, `authKeyName`). Pass a key (`@SupabaseCtx('supabase')`) to extract a single field.

### Typing your database

The guard does not thread a `Database` generic, so `@SupabaseCtx()` resolves to `SupabaseContext<unknown>` by default. To get typed table access, annotate the parameter at the handler:

```ts
import type { SupabaseContext } from '@supabase/server'
import type { Database } from './database.types'

@Get()
async list(@SupabaseCtx() ctx: SupabaseContext<Database>) {
const { data } = await ctx.supabase.from('favorite_games').select()
return data
}
```

## Per-route auth

Apply different auth modes per controller or per handler — the closest `@UseGuards()` wins:

```ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common'
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
import type { SupabaseContext } from '@supabase/server'

@Controller()
export class AppController {
// Public — no guard
@Get('health')
health() {
return { status: 'ok' }
}

// User-authenticated route
@Get('todos')
@UseGuards(withSupabase({ auth: 'user' }))
async todos(@SupabaseCtx() ctx: SupabaseContext) {
const { data } = await ctx.supabase.from('todos').select()
return data
}

// Secret-key-protected admin route
@Post('admin/sync')
@UseGuards(withSupabase({ auth: 'secret' }))
async sync(@SupabaseCtx() ctx: SupabaseContext) {
const { data } = await ctx.supabaseAdmin
.from('audit_log')
.insert({ action: 'sync' })
return data
}

// Dual auth — users or services
@Get('reports')
@UseGuards(withSupabase({ auth: ['user', 'secret'] }))
reports(@SupabaseCtx('authMode') authMode: SupabaseContext['authMode']) {
return { authMode }
}
}
```

## App-wide guard

Apply the guard globally with `app.useGlobalGuards()`:

```ts
// main.ts
import { NestFactory } from '@nestjs/core'
import { withSupabase } from '@supabase/server/adapters/nestjs'
import { AppModule } from './app.module'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
await app.listen(3000)
}
bootstrap()
```

## Skip behavior

If a previous guard or middleware already populated `request.supabaseContext`, subsequent `withSupabase` guards skip auth and reuse the existing context. This lets a global guard set a baseline auth mode while controller- or handler-level guards apply a stricter one — but only if the stricter guard runs first.

NestJS executes guards in this order: global → controller → handler. Because of that, a stricter handler-level guard cannot override a more permissive global guard via the skip mechanism. If you need different auth per route, prefer per-route `@UseGuards(...)` without a global guard.

## CORS

The NestJS adapter does not handle CORS. Use NestJS's built-in CORS:

```ts
// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableCors({ origin: 'https://myapp.com' })
await app.listen(3000)
}
bootstrap()
```

The `cors` option is excluded from `WithSupabaseConfig` for this adapter.

## Error handling

When auth fails, the adapter throws a NestJS `HttpException`. The original `AuthError` is available via `cause`. Add an exception filter to format the response:

```ts
// supabase-auth.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common'
import { AuthError } from '@supabase/server'
import type { Response } from 'express'

@Catch(HttpException)
export class SupabaseAuthFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const cause = exception.cause
if (!(cause instanceof AuthError)) throw exception

const res = host.switchToHttp().getResponse<Response>()
res.status(cause.status).json({
error: cause.message,
code: cause.code,
})
}
}
```

Register it globally:

```ts
// main.ts
app.useGlobalFilters(new SupabaseAuthFilter())
```

## Environment overrides

Pass `env` to override auto-detected environment variables:

```ts
@UseGuards(
withSupabase({
auth: 'user',
env: { url: 'http://localhost:54321' },
}),
)
```

## Supabase client options

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

```ts
@UseGuards(
withSupabase({
auth: 'user',
supabaseOptions: { db: { schema: 'api' } },
}),
)
```
14 changes: 4 additions & 10 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@
"exports": {
".": "./src/index.ts",
"./core": "./src/core/index.ts",
"./adapters/hono": "./src/adapters/hono/index.ts"
"./adapters/hono": "./src/adapters/hono/index.ts",
"./adapters/nestjs": "./src/adapters/nestjs/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"]
}
}
20 changes: 20 additions & 0 deletions 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/nestjs": {
"types": "./dist/adapters/nestjs/index.d.mts",
"import": "./dist/adapters/nestjs/index.mjs",
"require": "./dist/adapters/nestjs/index.cjs"
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -74,11 +79,15 @@
]
},
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@supabase/supabase-js": "^2.0.0",
"h3": "^2.0.0",
"hono": "^4.0.0"
},
"peerDependenciesMeta": {
"@nestjs/common": {
"optional": true
},
"h3": {
"optional": true
},
Expand All @@ -89,17 +98,28 @@
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@nestjs/common": "^11.1.19",
"@nestjs/core": "^11.1.19",
"@nestjs/platform-express": "^11.1.19",
"@nestjs/platform-fastify": "^11.1.19",
"@nestjs/testing": "^11.1.19",
"@supabase/supabase-js": "^2.98.0",
"@swc/core": "^1.15.33",
"@types/supertest": "^7.2.0",
"eslint": "^10.0.2",
"h3": "2.0.1-rc.20",
"hono": "^4.12.5",
"prettier": "3.8.1",
"pretty-quick": "^4.2.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"simple-git-hooks": "^2.13.1",
"supertest": "^7.2.2",
"tsdown": "^0.20.3",
"typedoc": "^0.28.19",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"unplugin-swc": "^1.5.9",
"vitest": "^4.0.18"
},
"dependencies": {
Expand Down
Loading