Skip to content

Commit 5aa7b41

Browse files
committed
Merge branch 'main' of https://github.com/supabase/server into FUNC-655/defineAdapter-for-uniform-integrations
2 parents 37ee19a + 3052a6b commit 5aa7b41

16 files changed

Lines changed: 2712 additions & 86 deletions

README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,12 @@ Adapters wrap `withSupabase` for a specific framework's middleware contract. The
266266
267267
> **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.
268268
269-
| Framework | Import | Framework version | Docs |
270-
| --------- | ---------------------------------- | ----------------- | -------------------------------------------------- |
271-
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
272-
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
273-
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |
269+
| Framework | Import | Framework version | Docs |
270+
| --------- | ---------------------------------- | ---------------------- | -------------------------------------------------- |
271+
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
272+
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
273+
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |
274+
| NestJS | `@supabase/server/adapters/nestjs` | `^10.0.0 \|\| ^11.0.0` | [docs/adapters/nestjs.md](docs/adapters/nestjs.md) |
274275
275276
See the per-adapter docs above for setup, per-route auth, CORS, error handling, and other patterns.
276277
@@ -316,6 +317,25 @@ app.listen(3000)
316317
317318
The adapter does not handle CORS — use `@elysiajs/cors` for that.
318319
320+
### NestJS
321+
322+
```ts
323+
import { Controller, Get, UseGuards } from '@nestjs/common'
324+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
325+
import type { SupabaseContext } from '@supabase/server'
326+
327+
@Controller('games')
328+
@UseGuards(withSupabase({ auth: 'user' }))
329+
export class GamesController {
330+
@Get()
331+
list(@SupabaseCtx() ctx: SupabaseContext) {
332+
return ctx.supabase.from('favorite_games').select()
333+
}
334+
}
335+
```
336+
337+
See [docs/adapters/nestjs.md](docs/adapters/nestjs.md) for per-route auth, exception filters, CORS, and more.
338+
319339
## Primitives
320340
321341
For when you need more control than `withSupabase` provides — multiple routes with different auth, custom response headers, or building your own wrapper.
@@ -465,6 +485,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
465485
| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
466486
| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
467487
| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) |
488+
| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) |
468489
469490
## Documentation
470491
@@ -476,6 +497,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
476497
| How do I use this with Hono? | [`docs/adapters/hono.md`](docs/adapters/hono.md) |
477498
| How do I use this with H3 / Nuxt? | [`docs/adapters/h3.md`](docs/adapters/h3.md) |
478499
| How do I use this with Elysia? | [`docs/adapters/elysia.md`](docs/adapters/elysia.md) |
500+
| How do I use this with NestJS? | [`docs/adapters/nestjs.md`](docs/adapters/nestjs.md) |
479501
| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) |
480502
| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) |
481503
| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) |

docs/adapters/nestjs.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# NestJS Adapter
2+
3+
## Setup
4+
5+
Install NestJS as a peer dependency:
6+
7+
```bash
8+
pnpm add @nestjs/common @nestjs/core
9+
```
10+
11+
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.
12+
13+
`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()`.
14+
15+
## Basic controller with auth
16+
17+
```ts
18+
// games.controller.ts
19+
import { Controller, Get, UseGuards } from '@nestjs/common'
20+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
21+
import type { SupabaseContext } from '@supabase/server'
22+
23+
@Controller('games')
24+
@UseGuards(withSupabase({ auth: 'user' }))
25+
export class GamesController {
26+
@Get()
27+
async list(@SupabaseCtx() ctx: SupabaseContext) {
28+
const { data } = await ctx.supabase.from('favorite_games').select()
29+
return data
30+
}
31+
32+
@Get('me')
33+
me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
34+
return user
35+
}
36+
}
37+
```
38+
39+
`@SupabaseCtx()` returns the entire `SupabaseContext` (`supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, `authMode`, `authKeyName`). Pass a key (`@SupabaseCtx('supabase')`) to extract a single field.
40+
41+
### Typing your database
42+
43+
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:
44+
45+
```ts
46+
import type { SupabaseContext } from '@supabase/server'
47+
import type { Database } from './database.types'
48+
49+
@Get()
50+
async list(@SupabaseCtx() ctx: SupabaseContext<Database>) {
51+
const { data } = await ctx.supabase.from('favorite_games').select()
52+
return data
53+
}
54+
```
55+
56+
## Per-route auth
57+
58+
Apply different auth modes per controller or per handler — the closest `@UseGuards()` wins:
59+
60+
```ts
61+
import { Controller, Get, Post, UseGuards } from '@nestjs/common'
62+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
63+
import type { SupabaseContext } from '@supabase/server'
64+
65+
@Controller()
66+
export class AppController {
67+
// Public — no guard
68+
@Get('health')
69+
health() {
70+
return { status: 'ok' }
71+
}
72+
73+
// User-authenticated route
74+
@Get('todos')
75+
@UseGuards(withSupabase({ auth: 'user' }))
76+
async todos(@SupabaseCtx() ctx: SupabaseContext) {
77+
const { data } = await ctx.supabase.from('todos').select()
78+
return data
79+
}
80+
81+
// Secret-key-protected admin route
82+
@Post('admin/sync')
83+
@UseGuards(withSupabase({ auth: 'secret' }))
84+
async sync(@SupabaseCtx() ctx: SupabaseContext) {
85+
const { data } = await ctx.supabaseAdmin
86+
.from('audit_log')
87+
.insert({ action: 'sync' })
88+
return data
89+
}
90+
91+
// Dual auth — users or services
92+
@Get('reports')
93+
@UseGuards(withSupabase({ auth: ['user', 'secret'] }))
94+
reports(@SupabaseCtx('authMode') authMode: SupabaseContext['authMode']) {
95+
return { authMode }
96+
}
97+
}
98+
```
99+
100+
## App-wide guard
101+
102+
Apply the guard globally with `app.useGlobalGuards()`:
103+
104+
```ts
105+
// main.ts
106+
import { NestFactory } from '@nestjs/core'
107+
import { withSupabase } from '@supabase/server/adapters/nestjs'
108+
import { AppModule } from './app.module'
109+
110+
async function bootstrap() {
111+
const app = await NestFactory.create(AppModule)
112+
app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
113+
await app.listen(3000)
114+
}
115+
bootstrap()
116+
```
117+
118+
## Multiple guards
119+
120+
`withSupabase` always runs, even if a previous guard already set `request.supabaseContext`. NestJS executes guards in order (global → controller → handler), so a handler-level guard naturally tightens what a global guard set: the later guard re-authenticates with its own config and either rejects the request or overwrites the context. The innermost guard wins.
121+
122+
If you need different auth per route, prefer per-route `@UseGuards(...)` without a global guard.
123+
124+
## CORS
125+
126+
The NestJS adapter does not handle CORS. Use NestJS's built-in CORS:
127+
128+
```ts
129+
// main.ts
130+
import { NestFactory } from '@nestjs/core'
131+
import { AppModule } from './app.module'
132+
133+
async function bootstrap() {
134+
const app = await NestFactory.create(AppModule)
135+
app.enableCors({ origin: 'https://myapp.com' })
136+
await app.listen(3000)
137+
}
138+
bootstrap()
139+
```
140+
141+
The `cors` option is excluded from `WithSupabaseConfig` for this adapter.
142+
143+
## Error handling
144+
145+
When auth fails, the adapter throws a NestJS `HttpException`. The original `AuthError` is available via `cause`. Add an exception filter to format the response:
146+
147+
```ts
148+
// supabase-auth.filter.ts
149+
import {
150+
ArgumentsHost,
151+
Catch,
152+
ExceptionFilter,
153+
HttpException,
154+
} from '@nestjs/common'
155+
import { AuthError } from '@supabase/server'
156+
import type { Response } from 'express'
157+
158+
@Catch(HttpException)
159+
export class SupabaseAuthFilter implements ExceptionFilter {
160+
catch(exception: HttpException, host: ArgumentsHost) {
161+
const cause = exception.cause
162+
if (!(cause instanceof AuthError)) throw exception
163+
164+
const res = host.switchToHttp().getResponse<Response>()
165+
res.status(cause.status).json({
166+
error: cause.message,
167+
code: cause.code,
168+
})
169+
}
170+
}
171+
```
172+
173+
Register it globally:
174+
175+
```ts
176+
// main.ts
177+
app.useGlobalFilters(new SupabaseAuthFilter())
178+
```
179+
180+
## Environment overrides
181+
182+
Pass `env` to override auto-detected environment variables:
183+
184+
```ts
185+
@UseGuards(
186+
withSupabase({
187+
auth: 'user',
188+
env: { url: 'http://localhost:54321' },
189+
}),
190+
)
191+
```
192+
193+
## Supabase client options
194+
195+
Forward options to the underlying `createClient()` calls:
196+
197+
```ts
198+
@UseGuards(
199+
withSupabase({
200+
auth: 'user',
201+
supabaseOptions: { db: { schema: 'api' } },
202+
}),
203+
)
204+
```

jsr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"./core/adapters": "./src/core/adapters/index.ts",
88
"./adapters/hono": "./src/adapters/hono/index.ts",
99
"./adapters/h3": "./src/adapters/h3/index.ts",
10-
"./adapters/elysia": "./src/adapters/elysia/index.ts"
10+
"./adapters/elysia": "./src/adapters/elysia/index.ts",
11+
"./adapters/nestjs": "./src/adapters/nestjs/index.ts"
1112
},
1213
"publish": {
1314
"include": ["src/**/*.ts", "README.md", "LICENSE"],

package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"import": "./dist/adapters/elysia/index.mjs",
5050
"require": "./dist/adapters/elysia/index.cjs"
5151
},
52+
"./adapters/nestjs": {
53+
"types": "./dist/adapters/nestjs/index.d.mts",
54+
"import": "./dist/adapters/nestjs/index.mjs",
55+
"require": "./dist/adapters/nestjs/index.cjs"
56+
},
5257
"./package.json": "./package.json"
5358
},
5459
"main": "./dist/index.cjs",
@@ -73,19 +78,23 @@
7378
"prepare": "simple-git-hooks",
7479
"test": "vitest run",
7580
"test:watch": "vitest",
76-
"typecheck": "tsc --noEmit"
81+
"typecheck": "tsc --noEmit && tsc --noEmit -p src/adapters/nestjs"
7782
},
7883
"simple-git-hooks": {
7984
"pre-commit": "pnpm pretty-quick --staged",
8085
"commit-msg": "pnpm commitlint --edit \"$1\""
8186
},
8287
"peerDependencies": {
88+
"@nestjs/common": "^10.0.0 || ^11.0.0",
8389
"@supabase/supabase-js": "^2.0.0",
8490
"h3": "^2.0.0",
8591
"hono": "^4.0.0",
8692
"elysia": "^1.4.0"
8793
},
8894
"peerDependenciesMeta": {
95+
"@nestjs/common": {
96+
"optional": true
97+
},
8998
"h3": {
9099
"optional": true
91100
},
@@ -99,18 +108,29 @@
99108
"devDependencies": {
100109
"@commitlint/cli": "^20.4.2",
101110
"@commitlint/config-conventional": "^20.4.2",
111+
"@nestjs/common": "^11.1.19",
112+
"@nestjs/core": "^11.1.19",
113+
"@nestjs/platform-express": "^11.1.19",
114+
"@nestjs/platform-fastify": "^11.1.19",
115+
"@nestjs/testing": "^11.1.19",
102116
"@supabase/supabase-js": "^2.105.4",
117+
"@swc/core": "^1.15.33",
118+
"@types/supertest": "^7.2.0",
103119
"eslint": "^10.0.2",
104120
"elysia": "^1.4.0",
105121
"h3": "2.0.1-rc.20",
106122
"hono": "^4.12.5",
107123
"prettier": "3.8.1",
108124
"pretty-quick": "^4.2.2",
125+
"reflect-metadata": "^0.2.2",
126+
"rxjs": "^7.8.2",
109127
"simple-git-hooks": "^2.13.1",
128+
"supertest": "^7.2.2",
110129
"tsdown": "^0.20.3",
111130
"typedoc": "^0.28.19",
112131
"typescript": "^5.9.3",
113132
"typescript-eslint": "^8.56.1",
133+
"unplugin-swc": "^1.5.9",
114134
"vitest": "^4.0.18"
115135
},
116136
"dependencies": {

0 commit comments

Comments
 (0)