Skip to main content

Lifecycle Hooks

Add custom logic at specific points in the authentication flow --- block signups based on business rules, send notifications, sync to external systems, or audit events.

nauth-toolkit provides 13 hooks across user lifecycle, security, and account management events. See Lifecycle Hooks Concept for the full list, execution model, and API reference.

Prerequisites

Step 1: Create Your Hook

This example sends a custom email when a user changes their password:

src/auth/hooks/password-changed-email.hook.ts
import { Injectable } from '@nestjs/common';
import {
PasswordChangedHook,
IPasswordChangedHook,
PasswordChangedMetadata,
} from '@nauth-toolkit/nestjs';

@Injectable()
@PasswordChangedHook({ priority: 1 })
export class PasswordChangedEmailHook implements IPasswordChangedHook {
constructor(private readonly emailService: EmailService) {}

async execute(metadata: PasswordChangedMetadata): Promise<void> {
await this.emailService.sendPasswordChangedAlert({
to: metadata.user.email,
changedBy: metadata.changedBy,
timestamp: new Date(),
sessionsRevoked: metadata.sessionsRevoked,
});
}
}

Step 2: Register Your Hook

src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthModule, NAuthHooksModule } from '@nauth-toolkit/nestjs';
import { authConfig } from './auth.config';
import { PasswordChangedEmailHook } from './hooks/password-changed-email.hook';

@Module({
imports: [
AuthModule.forRoot(authConfig),
NAuthHooksModule.forFeature([PasswordChangedEmailHook]),
],
})
export class CustomAuthModule {}

The hook is automatically discovered and registered.

Best Practices

Keep hooks focused

Each hook should have a single responsibility:

// Good - Single responsibility
@PasswordChangedHook()
export class PasswordChangedEmailHook {}

@PasswordChangedHook()
export class PasswordChangedAnalyticsHook {}

// Bad - Multiple responsibilities
@PasswordChangedHook()
export class PasswordChangedEverythingHook {
// Sends email, logs to analytics, syncs to CRM — too many things
}

Handle errors gracefully

Non-blocking hooks should handle errors explicitly:

async execute(metadata) {
try {
await this.emailService.send(metadata.user.email);
} catch (error) {
this.logger.error('Email failed:', error);
await this.queueService.add('retry-email', { userId: metadata.user.id });
}
}

Use dependency injection

Leverage framework DI for testability:

@Injectable()
@PasswordChangedHook({ priority: 1 })
export class PasswordChangedEmailHook implements IPasswordChangedHook {
constructor(
private readonly emailService: EmailService,
private readonly logger: Logger,
) {}
}

What's Next