NestJS Supabase Auth Passport Strategy
nestjs-supabase-auth is a NestJS Passport strategy designed to integrate Supabase authentication into NestJS applications. It leverages `passport-jwt` to validate JWTs issued by Supabase, allowing developers to secure their API routes and GraphQL resolvers. The package is currently at version 1.0.9 and generally maintains a stable release cadence, with updates primarily focused on bug fixes or adapting to changes in Supabase Auth, rather than frequent major breaking changes. Its key differentiator is providing a pre-built, opinionated integration for Supabase's JWT-based authentication within the established NestJS Passport ecosystem, simplifying the process of securing backends compared to implementing a generic JWT strategy and handling Supabase-specific claims manually. Users must extend the provided `SupabaseAuthStrategy` to configure their specific Supabase instance details and JWT secret, enabling flexible environment variable integration and custom user payload validation.
Common errors
-
Error: Cannot find module 'passport-jwt' or '@nestjs/passport'
cause Required peer dependencies for `nestjs-supabase-auth` are not installed.fixRun `npm install passport passport-jwt @nestjs/passport @types/passport-jwt --save-dev` to install all necessary peer dependencies. -
401 Unauthorized: Invalid Token or jwt malformed
cause The JWT provided in the 'Authorization: Bearer <token>' header is either missing, malformed, expired, or signed with a different secret than `supabaseJwtSecret` configured in the strategy.fixEnsure the client is sending a valid, unexpired Supabase access token. Double-check that `supabaseJwtSecret` in your `SupabaseJwtStrategy` exactly matches the 'JWT Secret' found in your Supabase project's API Settings. -
TypeError: Cannot read properties of undefined (reading 'user')
cause The `req.user` object is undefined, typically because the Passport strategy's `validate` method did not return a user object or the authentication guard was not correctly applied.fixVerify that your `SupabaseJwtStrategy`'s `validate` method explicitly returns an object (e.g., `{ userId: payload.sub, ... }`). Also, ensure `@UseGuards(AuthGuard('supabase'))` is correctly applied to your controller methods or resolvers. -
Nest can't resolve dependencies of the SupabaseJwtStrategy (?). Please make sure that the argument at index [0] is available in the AuthModule context.
cause This usually indicates that `PassportModule` is not imported into your `AuthModule`, or `@Injectable()` is missing on your strategy.fixEnsure `PassportModule` is imported into your `AuthModule`'s `imports` array (`imports: [PassportModule]`). Also, confirm `SupabaseJwtStrategy` has the `@Injectable()` decorator.
Warnings
- gotcha This package requires several peer dependencies (`passport`, `passport-jwt`, `@nestjs/passport`) to be installed manually alongside it. Failure to install these will result in runtime errors related to missing modules or `npm install` warnings.
- breaking The `supabaseJwtSecret` option is critical for JWT verification. It must exactly match the JWT secret configured in your Supabase project settings. Mismatches will cause all JWTs to be rejected as invalid, leading to 401 Unauthorized errors.
- gotcha When extending `SupabaseAuthStrategy`, the `validate` method is where you determine what user data is attached to `req.user`. If you override `validate` without returning a meaningful user object, `req.user` will be `undefined` in your controllers/resolvers.
- deprecated The `validate` and `authenticate` methods shown in the example README sometimes include `super.validate(payload);` and `super.authenticate(req);`. While technically allowed, the base `SupabaseAuthStrategy`'s `validate` method currently only logs the payload and doesn't perform additional checks beyond what `passport-jwt` already does. `super.authenticate` generally performs the core Passport.js flow.
Install
-
npm install nestjs-supabase-auth -
yarn add nestjs-supabase-auth -
pnpm add nestjs-supabase-auth
Imports
- SupabaseAuthStrategy
const SupabaseAuthStrategy = require('nestjs-supabase-auth');import { SupabaseAuthStrategy } from 'nestjs-supabase-auth'; - PassportStrategy
import { Strategy } from '@nestjs/passport';import { PassportStrategy } from '@nestjs/passport'; - ExtractJwt
import * as ExtractJwt from 'passport-jwt';
import { ExtractJwt } from 'passport-jwt'; - AuthGuard
import { AuthGuard } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';
Quickstart
import { Injectable, Module } from '@nestjs/common';
import { PassportStrategy, AuthGuard } from '@nestjs/passport';
import { ExtractJwt } from 'passport-jwt';
import { SupabaseAuthStrategy } from 'nestjs-supabase-auth';
import { PassportModule } from '@nestjs/passport';
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
// --- Strategy Definition (supabase.strategy.ts) ---
@Injectable()
export class SupabaseJwtStrategy extends PassportStrategy(
SupabaseAuthStrategy,
'supabase',
) {
public constructor() {
super({
supabaseUrl: process.env.SUPABASE_URL ?? 'https://your-project-ref.supabase.co',
supabaseKey: process.env.SUPABASE_KEY ?? 'YOUR_SUPABASE_ANON_KEY',
supabaseOptions: {},
supabaseJwtSecret: process.env.SUPABASE_JWT_SECRET ?? 'YOUR_SUPABASE_JWT_SECRET',
extractor: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
async validate(payload: any): Promise<any> {
// This method is called after JWT verification. 'payload' contains the decoded JWT.
// You can perform additional user validation or data fetching here.
// Ensure the `sub` claim (user ID) is present.
if (!payload || !payload.sub) {
throw new Error('Invalid JWT payload: Missing user ID.');
}
// IMPORTANT: Call super.validate(payload) if you need the base strategy's validation logic
// or omit it if you fully override the validation.
// super.validate(payload); // Base validation might be empty or specific to the original strategy.
// Return the validated user payload. NestJS will attach this to req.user.
return { userId: payload.sub, email: payload.email, ...payload };
}
authenticate(req: Request) {
// This method can be overridden for custom authentication logic before validation.
// In most cases, the default Passport.js flow is sufficient.
super.authenticate(req);
}
}
// --- Auth Module (auth.module.ts) ---
@Module({
imports: [PassportModule],
providers: [SupabaseJwtStrategy],
exports: [SupabaseJwtStrategy, PassportModule], // Export PassportModule if other modules need it
})
export class AuthModule {}
// --- Protected Controller (user.controller.ts) ---
const SUPABASE_AUTH_GUARD = 'supabase'; // Define the guard name consistently
@Controller('user')
export class UserController {
@UseGuards(AuthGuard(SUPABASE_AUTH_GUARD))
@Get('profile')
getProfile(@Request() req) {
// req.user will contain the object returned by the validate method
return req.user;
}
}
// --- Main Application (main.ts or app.module.ts, simplified for quickstart) ---
// This setup assumes AuthModule is imported into AppModule.
// You would also need to configure your NestJS application to load environment variables.
// Example App Module might look like:
// @Module({
// imports: [AuthModule],
// controllers: [UserController],
// })
// export class AppModule {}
// To run this, you'd typically have a NestJS app initialized with `nest new`,
// then add these files and configure environment variables:
// SUPABASE_URL=https://<your-project-ref>.supabase.co
// SUPABASE_KEY=<your-anon-public-key>
// SUPABASE_JWT_SECRET=<your-jwt-secret-from-supabase-settings>