Skip to main content

@PostSignupHook()

Package: @nauth-toolkit/nestjs Type: Class Decorator

Class decorator that automatically registers a provider as a post-signup hook. Post-signup hooks execute after successful user creation for notifications, integrations, and analytics. Non-blocking - errors are logged but don't affect signup.

Import from NestJS Package
import { PostSignupHook } from '@nauth-toolkit/nestjs';

Overview

The @PostSignupHook() decorator enables automatic hook registration without manual setup. Classes decorated with this decorator are discovered at module initialization and registered with the HookRegistryService.

Key Features:

  • Automatic hook discovery and registration
  • No manual registry calls required
  • Full dependency injection support
  • Priority-based execution ordering
  • Non-blocking - errors don't affect signup
  • Type-safe with TypeScript

When Post-Signup Hooks Execute:

  • After password signup completes
  • After social signup completes
  • After admin signup completes

Usage

Basic Hook

import { Injectable, Logger } from '@nestjs/common';
import { PostSignupHook, IPostSignupHookProvider } from '@nauth-toolkit/nestjs';
import { EmailService } from '../services/email.service';

@Injectable()
@PostSignupHook()
export class WelcomeEmailHook implements IPostSignupHookProvider {
private readonly logger = new Logger(WelcomeEmailHook.name);

constructor(private readonly emailService: EmailService) {}

async execute(user, metadata) {
try {
this.logger.log(`Sending welcome email to: ${user.email}`);

await this.emailService.sendWelcome({
to: user.email,
firstName: user.firstName,
signupMethod: metadata?.signupType,
});

this.logger.log(`Welcome email sent to: ${user.email}`);
} catch (error) {
// Errors are logged but don't block signup
this.logger.error(`Failed to send welcome email: ${error.message}`);
}
}
}

With Priority

Control execution order using the priority option. Lower priority values execute first:

import { Injectable } from '@nestjs/common';
import { PostSignupHook, IPostSignupHookProvider } from '@nauth-toolkit/nestjs';

@Injectable()
@PostSignupHook({ priority: 1 }) // Executes first
export class WelcomeEmailHook implements IPostSignupHookProvider {
async execute(user, metadata) {
// Send welcome email
}
}

@Injectable()
@PostSignupHook({ priority: 2 }) // Executes second
export class AnalyticsHook implements IPostSignupHookProvider {
async execute(user, metadata) {
// Track signup event
}
}

Default Priority: If not specified, priority defaults to 100.

With Dependency Injection

Hooks support full NestJS dependency injection:

import { Injectable, Logger } from '@nestjs/common';
import { PostSignupHook, IPostSignupHookProvider } from '@nauth-toolkit/nestjs';
import { EmailService } from '../services/email.service';
import { AnalyticsService } from '../services/analytics.service';
import { CrmService } from '../services/crm.service';

@Injectable()
@PostSignupHook()
export class UserProvisioningHook implements IPostSignupHookProvider {
private readonly logger = new Logger(UserProvisioningHook.name);

constructor(
private readonly emailService: EmailService,
private readonly analytics: AnalyticsService,
private readonly crm: CrmService,
) {}

async execute(user, metadata) {
try {
// Send welcome email
await this.emailService.sendWelcome(user.email);

// Track signup event
await this.analytics.track('user_signup', {
userId: user.sub,
signupMethod: metadata?.signupType,
provider: metadata?.provider,
});

// Sync to CRM
await this.crm.createContact({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
});

this.logger.log(`User provisioning complete for: ${user.email}`);
} catch (error) {
this.logger.error(`User provisioning failed: ${error.message}`);
// Error doesn't block signup
}
}
}

Module Registration

Register hooks using NAuthHooksModule.forFeature():

import { Module } from '@nestjs/common';
import { AuthModule, NAuthHooksModule } from '@nauth-toolkit/nestjs';
import { authConfig } from './auth.config';
import { WelcomeEmailHook } from './hooks/welcome-email.hook';
import { AnalyticsHook } from './hooks/analytics.hook';

@Module({
imports: [AuthModule.forRoot(authConfig), NAuthHooksModule.forFeature([WelcomeEmailHook, AnalyticsHook])],
providers: [EmailService, AnalyticsService], // Hook dependencies
})
export class CustomAuthModule {}

Error Handling

Post-signup hooks are non-blocking. All errors are caught and logged automatically:

@Injectable()
@PostSignupHook()
export class NotificationHook implements IPostSignupHookProvider {
async execute(user, metadata) {
// If this throws, error is logged but signup continues
await this.notificationService.send(user.email);

// Subsequent code still executes
await this.otherService.doSomething();
}
}

Execution Behavior:

  • All hooks execute regardless of errors
  • Errors are caught and logged per hook
  • Signup is never blocked
  • Next hook always executes

Best Practice: Handle errors explicitly within your hook for better control:

async execute(user, metadata) {
try {
await this.emailService.sendWelcome(user.email);
} catch (error) {
this.logger.error('Welcome email failed:', error);
// Optional: Queue for retry
await this.queueService.add('welcome-email', { userId: user.id });
}
}

Using Signup Metadata

The metadata parameter provides context about the signup:

@Injectable()
@PostSignupHook()
export class ContextualEmailHook implements IPostSignupHookProvider {
async execute(user, metadata) {
if (metadata?.signupType === 'social') {
// Social signup - use profile picture and social metadata
await this.emailService.sendSocialWelcome({
email: user.email,
provider: metadata.provider, // 'google', 'apple', 'facebook'
profilePicture: metadata.profilePicture, // Profile picture URL
locale: metadata.socialMetadata?.locale as string | undefined, // From social metadata
});
} else {
// Password signup
if (metadata?.requiresVerification) {
// Post-signup runs BEFORE verification challenges complete.
// Use this for reminders/analytics, not for “welcome” emails.
await this.emailService.sendVerificationReminder(user.email);
} else {
// If no verification is required, onboarding is already complete.
// (If verification IS required, send welcome after onboarding completion instead.)
await this.emailService.sendWelcome(user.email);
}
}

// Track admin-initiated signups separately
if (metadata?.adminSignup) {
await this.analytics.track('admin_created_user', { userId: user.sub });
}
}
}

Using Social Metadata:

For social signups, you can access the complete OAuth profile data:

@Injectable()
@PostSignupHook()
export class ProfileSetupHook implements IPostSignupHookProvider {
async execute(user, metadata) {
if (metadata?.signupType === 'social' && metadata.socialMetadata) {
// Extract additional data from social provider
const socialData = metadata.socialMetadata;
const locale = socialData.locale as string | undefined;
const timezone = socialData.timezone as string | undefined;

// Update user profile with social data
await this.userService.updateProfile(user.sub, {
locale,
timezone,
profilePictureUrl: metadata.profilePicture,
});

// Sync to external systems with full OAuth profile
await this.crmService.createContact({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
profilePicture: metadata.profilePicture,
source: `social_${metadata.provider}`,
metadata: metadata.socialMetadata, // Full OAuth profile data
});
}
}
}

Testing

Hooks are regular NestJS providers and can be unit tested:

import { Test } from '@nestjs/testing';
import { WelcomeEmailHook } from './welcome-email.hook';
import { EmailService } from '../services/email.service';

describe('WelcomeEmailHook', () => {
let hook: WelcomeEmailHook;
let emailService: jest.Mocked<EmailService>;

beforeEach(async () => {
const mockEmailService = {
sendWelcome: jest.fn(),
};

const module = await Test.createTestingModule({
providers: [WelcomeEmailHook, { provide: EmailService, useValue: mockEmailService }],
}).compile();

hook = module.get(WelcomeEmailHook);
emailService = module.get(EmailService);
});

it('should send welcome email', async () => {
const user = {
sub: 'user-123',
email: 'user@example.com',
firstName: 'John',
};

await hook.execute(user, { signupType: 'password' });

expect(emailService.sendWelcome).toHaveBeenCalledWith({
to: 'user@example.com',
firstName: 'John',
signupMethod: 'password',
});
});

it('should not throw on email service error', async () => {
emailService.sendWelcome.mockRejectedValue(new Error('SMTP failed'));

const user = {
sub: 'user-123',
email: 'user@example.com',
firstName: 'John',
};

// Should not throw - errors are caught
await expect(hook.execute(user, { signupType: 'password' })).resolves.not.toThrow();
});
});