feat: add NestJS adapter#55
Conversation
Ships `@supabase/server/adapters/nestjs`:
- `withSupabase(opts)` — class guard for `@UseGuards()` and
`useGlobalGuards()`, supporting Express and Fastify
- `@SupabaseCtx(key?, ...pipes)` — param decorator returning the full
SupabaseContext or a single field, with NestJS pipes applied to the
extracted value
- 401s thrown as `HttpException` with `{ message, code }`; the
underlying `AuthError` is exposed on `cause`
Adds `@nestjs/common` as an optional peer dep (`^10 || ^11`), wires the
new export in package.json / jsr.json / tsdown.config.ts, and enables
`experimentalDecorators` + `emitDecoratorMetadata` in tsconfig. Test
setup uses unplugin-swc via vitest.config.ts so integration tests can
boot a real Nest app on both Express and Fastify.
Docs: README quickstart + docs/adapters/nestjs.md.
mandarini
left a comment
There was a problem hiding this comment.
Hi @bogdantarasenko ! Thank you very much for this PR and for contributing to Supabase! :D I left some change requests, can you please take a look?
| "experimentalDecorators": true, | ||
| "emitDecoratorMetadata": true |
There was a problem hiding this comment.
These two settings should be moved in a new scoped file:
src/adapters/nestjs/tsconfig.json
where you can write somethign like
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}so that these two settings are not enforced on the whole repo.
| // SWC handles legacy decorators with metadata for the NestJS integration test. | ||
| // The default vitest transform (esbuild) doesn't emit decorator metadata, which | ||
| // NestJS needs to wire up controllers, guards, and DI. |
There was a problem hiding this comment.
Ack, we can remove this comment
| plugins: [ | ||
| swc.vite({ | ||
| jsc: { | ||
| parser: { syntax: 'typescript', decorators: true }, | ||
| transform: { decoratorMetadata: true, legacyDecorator: true }, | ||
| }, | ||
| }), | ||
| ], |
There was a problem hiding this comment.
I think I would prefer here also to scope the swc setting only where it's needed (nestjs), so I think it would be better if we used the projects syntax:
export default defineConfig({
test: {
projects: [ ... ],
},
})so project 1 that would target all the tests would be like:
{
test: {
name: 'unit',
include: ['src/**/*.test.ts'],
exclude: ['src/adapters/nestjs/**'],
},
}, and project 2 for nest would be like
{
plugins: [
swc.vite({
jsc: {
parser: { syntax: 'typescript', decorators: true },
transform: { decoratorMetadata: true, legacyDecorator: true },
},
}),
],
test: {
name: 'nestjs',
include: ['src/adapters/nestjs/**/*.test.ts'],
},
}, | // or WebSocket contexts the request shape differs (no `headers`), so | ||
| // skip rather than crash. Users on those transports should authenticate | ||
| // via the appropriate context-specific mechanism. | ||
| if (executionContext.getType() !== 'http') return true |
There was a problem hiding this comment.
I have a small concern about the fail-open. Returning true for non-HTTP contexts means the guard silently allows every message on a WS gateway or RPC handler, so if someone wires it onto one of those expecting auth, they'd get a no-op with no runtime signal. I see the comment explaining the HTTP-only scope, which is totally fair, the guard genuinely can't read non-HTTP requests. But the skip behavior is what worries me, not the scoping. Two optiosn that I think are safer:
- Throw on non-HTTP contexts so misuse fails loudly on the first request rather than silently passing through. Same "we don't authenticate transports we can't read" intent, but enforced instead of advisory.
- Return false to deny by default.
If the current return true is the deliberate choice, I'd at least want the JSDoc on withSupabase to call out the HTTP-only constraint prominently. Right now a reader scanning the API could reasonably assume the guard authenticates whatever Nest hands it.
| // Skip if a previous guard already set the context. Enables stacking | ||
| // `@UseGuards(withSupabase({ auth: 'user' }))` at the controller level | ||
| // with a different auth mode at the handler level — the first one wins. | ||
| if (req.supabaseContext) return true |
There was a problem hiding this comment.
This makes me a bit nervous. Nest's guard order is fixed (global → controller → handler), so "first wins" here always means "outermost wins". Handler-level guards can never tighten what a global guard set. This, I think, could grant a user token access to a route the handler explicitly scoped to secret.
Summary
Adds a community NestJS adapter under
@supabase/server/adapters/nestjs, alongside the existing Hono and H3 adapters.withSupabase(opts)returns a class guard usable with@UseGuards()orapp.useGlobalGuards(new (withSupabase(...))()). Works on both Express and Fastify platforms; HTTP/2 pseudo-headers and non-HTTP execution contexts (rpc/ws) are handled.@SupabaseCtx(key?, ...pipes)is a param decorator that returns the fullSupabaseContextor a single field, with NestJS pipes applied to the extracted value.HttpExceptionwith{ message, code }; the underlyingAuthErroris exposed oncauseso consumers can build their own exception filters.@nestjs/commonis added as an optional peer dep (^10.0.0 || ^11.0.0), so users who don't import the adapter aren't forced to install Nest.What's in the diff
src/adapters/nestjs/{middleware,decorator,index}.ts— adapter sourcesrc/adapters/nestjs/{middleware,integration}.test.ts— unit tests for the guard/decorator plus integration tests that boot a real Nest app on Express and Fastify (covers@UseGuards,useGlobalGuards, and@SupabaseCtxwith pipes)vitest.config.ts— usesunplugin-swcso the integration tests getemitDecoratorMetadata, which Nest's DI requires (esbuild, vitest's default transform, doesn't emit it)tsconfig.json—experimentalDecorators+emitDecoratorMetadatafor the same reasontsdown.config.ts,package.jsonexports,jsr.json— wire the new entry pointREADME.md,src/adapters/README.md, newdocs/adapters/nestjs.md— documentationTest plan
pnpm test— unit + integration tests pass on Express and Fastifypnpm build—tsdownemitsdist/adapters/nestjs/index.{mjs,cjs,d.mts,d.cts}peerDependenciesMeta) is acceptable so non-Nest users aren't affectedvitest.config.ts+unplugin-swctest setup; it's only needed because Nest's DI requires decorator metadata that esbuild doesn't emit