@vitra-ai/nestjs-auth
Authentication package with Better Auth integration for NestJS applications. Provides a complete authentication solution with support for social providers, email OTP, magic links, and multi-organization support.
Features
- 🔐 Better Auth Integration: Full Better Auth support with NestJS
- 👥 Social Authentication: Google OAuth and more
- 📧 Email OTP & Magic Links: Passwordless authentication
- 🏢 Organizations: Multi-tenant organization support with roles
- 🛡️ Guards: Ready-to-use authentication guards (Auth, Admin, Organization)
- 🔑 Access Control: Role-based and organization-scoped permissions system
- 📦 Sequelize Models: Pre-configured database models
- 🎯 Type-safe: Full TypeScript support
Installation
npm install @vitra-ai/nestjs-auth
yarn add @vitra-ai/nestjs-auth
bun add @vitra-ai/nestjs-auth
Peer Dependencies
npm install @nestjs/common @nestjs/core @nestjs/config @nestjs/sequelize better-auth express pg sequelize sequelize-typescript
Quick Start
1. Create Auth Configuration
import { createAuth } from '@vitra-ai/nestjs-auth';
export const auth = createAuth({
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trustedOrigins: ['http://localhost:3000'],
secret: process.env.AUTH_SECRET!,
appName: 'My App',
database: {
host: process.env.DB_HOST!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
database: process.env.DB_NAME!,
ssl: {
rejectUnauthorized: false,
},
},
socialProviders: {
google: {
enabled: true,
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectURI: `${process.env.BASE_URL}/api/auth/callback/google`,
},
},
emailOTP: {
disableSignUp: true,
sendVerificationOTP: async ({ email, otp }) => {
console.log(`Send OTP ${otp} to ${email}`);
},
},
});
2. Set Up Your App Module
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import {
AuthModule,
User,
Session,
Account,
Verification,
Organization,
Member,
Invitation,
OrganizationRole,
} from '@vitra-ai/nestjs-auth';
import { auth } from './auth.config';
@Module({
imports: [
SequelizeModule.forRoot({
}),
AuthModule.forRoot(auth, {
imports: [
SequelizeModule.forFeature([
User,
Session,
Account,
Verification,
Organization,
Member,
Invitation,
OrganizationRole,
]),
],
}),
],
})
export class AppModule {}
3. Use Auth Guard
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@vitra-ai/nestjs-auth';
@Controller('protected')
@UseGuards(AuthGuard)
export class ProtectedController {
@Get()
getProtectedData() {
return { message: 'This is protected!' };
}
}
API Reference
createAuth(options)
Factory function to create a Better Auth instance with opinionated defaults.
Options:
{
baseURL: string;
trustedOrigins: string[];
secret: string;
appName: string;
database: {
host: string;
user: string;
password: string;
database: string;
ssl?: { rejectUnauthorized: boolean };
};
socialProviders?: {
google?: {
enabled: boolean;
clientId: string;
clientSecret: string;
redirectURI: string;
disableSignUp?: boolean;
scope?: string[];
};
};
emailOTP?: {
disableSignUp?: boolean;
sendVerificationOTP: (data) => Promise<void>;
};
magicLink?: {
sendMagicLink: (data) => Promise<void>;
};
additionalUserFields?: Record<string, any>;
additionalOptions?: BetterAuthOptions;
}
AuthModule
NestJS module for authentication.
Methods:
forRoot(auth, options?) - Configure module with auth instance
forRootAsync(options) - Configure module asynchronously
AuthGuard
Authentication guard for protecting routes.
Usage:
@UseGuards(AuthGuard)
@Controller('api')
export class ApiController {
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
AuthService
Service for accessing auth API.
@Injectable()
export class MyService {
constructor(private authService: AuthService) {}
async getSession(headers) {
return this.authService.api.getSession({ headers });
}
}
Models
Pre-configured Sequelize models:
User - User model
Session - Session model
Account - OAuth account model
Verification - Verification token model
Organization - Organization model
Member - Organization member model
Invitation - Organization invitation model
OrganizationRole - Organization role model
Environment Variables
# Required
AUTH_SECRET=your-secret-key-min-32-chars
BASE_URL=http://localhost:3000
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=myapp
# Optional - Google OAuth
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
Permissions & Access Control
The package includes a built-in role-based access control system powered by Better Auth's admin plugin with access control.
Permissions API
The PermissionsController is automatically included in the AuthModule, so you don't need to add it manually. Once you import AuthModule, the permissions endpoint is available:
GET /api/auth/permissions
Response:
{
"resources": [
{
"resource": "user",
"actions": ["create", "list", "set-role", "ban", "impersonate", "delete", "set-password"],
"permissions": ["user:create", "user:list", ...]
},
{
"resource": "project",
"actions": ["create", "share", "update", "delete"],
"permissions": ["project:create", "project:share", ...]
}
],
"flat": ["user:create", "user:list", ..., "project:create", ...]
}
Using the Admin Permission Guard
Protect routes with global admin-level permission checks using the @RequirePermissions decorator:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AdminPermissionGuard, RequirePermissions } from '@vitra-ai/nestjs-auth';
@Controller('admin')
@UseGuards(AdminPermissionGuard)
export class AdminController {
@Get('users')
@RequirePermissions({ user: ['list'] })
listUsers() {
return this.userService.findAll();
}
@Delete('users/:id')
@RequirePermissions({ user: ['delete'] })
deleteUser(@Param('id') id: string) {
return this.userService.delete(id);
}
}
Using the Organization Permission Guard
Protect routes with organization-scoped permission checks using the OrganizationPermissionGuard. This guard validates that users are members of the organization and have the required permissions within that organization context.
import { Controller, Get, Post, Param, UseGuards } from '@nestjs/common';
import {
AuthGuard,
OrganizationPermissionGuard,
RequireOrgPermissions,
OrgIdFrom,
} from '@vitra-ai/nestjs-auth';
@Controller('organizations/:orgId/projects')
@UseGuards(AuthGuard, OrganizationPermissionGuard)
export class ProjectController {
@Get()
@OrgIdFrom({ type: 'param', key: 'orgId' })
@RequireOrgPermissions({ permissions: { project: ['create'] } })
listProjects(@Param('orgId') orgId: string) {
return this.projectService.findAll(orgId);
}
@Post()
@OrgIdFrom({ type: 'param', key: 'orgId' })
@RequireOrgPermissions({ permissions: { project: ['create'] } })
createProject(@Param('orgId') orgId: string, @Body() dto: CreateProjectDto) {
return this.projectService.create(orgId, dto);
}
@Delete(':projectId')
@OrgIdFrom({ type: 'param', key: 'orgId' })
@RequireOrgPermissions({ roles: ['owner', 'admin'] })
deleteProject(@Param('orgId') orgId: string, @Param('projectId') projectId: string) {
return this.projectService.delete(orgId, projectId);
}
}
Key differences between guards:
| Scope | Global/Application-wide | Organization-specific |
| Membership Check | No | Yes |
| Organization Context | Not required | Required |
| Use Case | Admin-only features | Organization resources |
The @OrgIdFrom decorator specifies where to extract the organization ID from:
@OrgIdFrom({ type: 'param', key: 'orgId' })
@OrgIdFrom({ type: 'body', key: 'organizationId' })
@OrgIdFrom({ type: 'query', key: 'org' })
@OrgIdFrom({ type: 'header', key: 'x-organization-id' })
For detailed examples and best practices, see ORG_PERMISSION_GUARD.md.
Using Permissions in Services
import { Injectable, ForbiddenException } from '@nestjs/common';
import { roles, type Role } from '@vitra-ai/nestjs-auth';
@Injectable()
export class ProjectService {
async createProject(userRole: Role) {
if (!roles[userRole].authorize({ project: ['create'] }).success) {
throw new ForbiddenException('You do not have permission to create projects');
}
}
}
Customizing Permissions
You can extend or modify the permissions in your application:
import { createAccessControl } from 'better-auth/plugins/access';
import { defaultStatements } from 'better-auth/plugins/organization/access';
const customStatement = {
...defaultStatements,
project: ['create', 'share', 'update', 'delete'],
document: ['read', 'write', 'delete'],
report: ['view', 'generate', 'export'],
} as const;
export const customAc = createAccessControl(customStatement);
export const customAdmin = customAc.newRole({
organization: ['create', 'update', 'delete'],
project: ['create', 'update', 'delete'],
document: ['read', 'write', 'delete'],
report: ['view', 'generate', 'export'],
});
Available Default Roles
- admin: Full admin permissions (includes all user/session management from Better Auth admin plugin) + full project access
- manager: Can create and update projects but cannot delete
- user: Basic user with minimal permissions
Checking Permissions Server-Side
Use Better Auth's userHasPermission API:
import { AuthService } from '@vitra-ai/nestjs-auth';
import { fromNodeHeaders } from 'better-auth/api';
@Injectable()
export class MyService {
constructor(private authService: AuthService) {}
async checkUserPermission(headers: Record<string, string>) {
const result = await this.authService.api.userHasPermission({
headers: fromNodeHeaders(headers),
body: {
permissions: {
project: ['create'],
},
},
});
return result.success;
}
}
Advanced Usage
Custom Email Sending
createAuth({
emailOTP: {
sendVerificationOTP: async ({ email, otp, type }) => {
await yourEmailService.send({
to: email,
subject: 'Your verification code',
text: `Your code is: ${otp}`,
});
},
},
magicLink: {
sendMagicLink: async ({ email, url, token }) => {
await yourEmailService.send({
to: email,
subject: 'Sign in to your account',
html: `<a href="${url}">Click here to sign in</a>`,
});
},
},
});
Adding Custom User Fields
createAuth({
additionalUserFields: {
referralCode: {
type: 'string',
required: false,
},
subscriptionTier: {
type: 'string',
required: true,
},
},
});
License
MIT