Skip to content

feat: add NestJS adapter#55

Open
bogdantarasenko wants to merge 1 commit into
supabase:mainfrom
bogdantarasenko:feat/nestjs-adapter
Open

feat: add NestJS adapter#55
bogdantarasenko wants to merge 1 commit into
supabase:mainfrom
bogdantarasenko:feat/nestjs-adapter

Conversation

@bogdantarasenko
Copy link
Copy Markdown

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() or app.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 full SupabaseContext or a single field, with NestJS pipes applied to the extracted value.
  • Failed auth surfaces as HttpException with { message, code }; the underlying AuthError is exposed on cause so consumers can build their own exception filters.

@nestjs/common is 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 source
  • src/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 @SupabaseCtx with pipes)
  • vitest.config.ts — uses unplugin-swc so the integration tests get emitDecoratorMetadata, which Nest's DI requires (esbuild, vitest's default transform, doesn't emit it)
  • tsconfig.jsonexperimentalDecorators + emitDecoratorMetadata for the same reason
  • tsdown.config.ts, package.json exports, jsr.json — wire the new entry point
  • README.md, src/adapters/README.md, new docs/adapters/nestjs.md — documentation

Test plan

  • pnpm test — unit + integration tests pass on Express and Fastify
  • pnpm buildtsdown emits dist/adapters/nestjs/index.{mjs,cjs,d.mts,d.cts}
  • Reviewer: confirm the optional-peer-dep approach (peerDependenciesMeta) is acceptable so non-Nest users aren't affected
  • Reviewer: sanity-check the vitest.config.ts + unplugin-swc test setup; it's only needed because Nest's DI requires decorator metadata that esbuild doesn't emit

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.
Copy link
Copy Markdown
Collaborator

@mandarini mandarini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread tsconfig.json
Comment on lines +9 to +10
"experimentalDecorators": true,
"emitDecoratorMetadata": true
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread vitest.config.ts
Comment on lines +4 to +6
// 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, we can remove this comment

Comment thread vitest.config.ts
Comment on lines +8 to +15
plugins: [
swc.vite({
jsc: {
parser: { syntax: 'typescript', decorators: true },
transform: { decoratorMetadata: true, legacyDecorator: true },
},
}),
],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants