Skip to main content

Guards & Interceptors

The Ever Works API uses NestJS guards and interceptors as middleware layers for authentication, authorization, rate limiting, logging, and monitoring. This page documents all guards, interceptors, decorators, and middleware registered in the application.

Execution Pipeline

NestJS processes requests through a well-defined pipeline. Here is the order for the Ever Works API:

Incoming Request
--> Helmet Middleware (security headers)
--> CORS Middleware
--> Express body-parser (10MB limit)
--> Global Guards
├── JwtAuthGuard (authentication)
└── ThrottlerGuard (rate limiting)
--> Route-specific Guards
└── CrmSyncGuard, LocalAuthGuard, etc.
--> Global Interceptors
├── LoggingInterceptor
├── SentryInterceptor
└── PostHogInterceptor
--> Global Pipes
└── ValidationPipe
--> Controller Method
--> Response

Global Guards

Global guards are registered in ApiModule using the APP_GUARD token:

@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: ThrottlerGuard }
]
})
export class ApiModule {}

JwtAuthGuard

Source: apps/api/src/auth/guards/jwt-auth.guard.ts

The primary authentication guard, applied globally to all routes. It extends Passport's AuthGuard('jwt') and adds support for the @Public() decorator.

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}

canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass()
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

Behavior:

ScenarioResult
Valid JWT in Authorization: Bearer headerRequest proceeds; request.user populated
Missing or invalid JWT401 Unauthorized
Route decorated with @Public()Guard skipped; request proceeds without auth

Key Feature: The guard checks both the handler method and the controller class for the IS_PUBLIC_KEY metadata using getAllAndOverride, meaning @Public() can be applied at either level.

ThrottlerGuard

The global rate limiter from @nestjs/throttler, configured with three tiers:

TierTime WindowMax Requests
short1 second50
medium10 seconds300
long1 minute1000

When any tier limit is exceeded, the guard returns 429 Too Many Requests.

Route-Specific Guards

LocalAuthGuard

Source: apps/api/src/auth/guards/local-auth.guard.ts

Used on the login endpoint to authenticate with username/password credentials via Passport's local strategy.

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

Applied only to the login route:

@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) { ... }

CrmSyncGuard

Source: apps/api/src/integrations/twenty-crm/guards/crm-sync.guard.ts

A feature-flag guard that blocks CRM-related requests when the integration is disabled or misconfigured.

@Injectable()
export class CrmSyncGuard implements CanActivate {
constructor(private readonly configService: CrmConfigService) {}

canActivate(context: ExecutionContext): boolean {
if (!this.configService.isEnabled) {
this.logger.warn('CRM integration is disabled - request blocked');
return false;
}

try {
this.configService.validateConfig();
return true;
} catch (error) {
this.logger.error('CRM configuration validation failed:', error);
return false;
}
}
}
CheckFailure Result
isEnabled is false403 Forbidden with warning log
validateConfig() throws403 Forbidden with error log
Both passRequest proceeds

Global Interceptors

Registered in ApiModule using the APP_INTERCEPTOR token:

@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: SentryInterceptor },
{ provide: APP_INTERCEPTOR, useClass: PostHogInterceptor },
],
})

LoggingInterceptor

Source: apps/api/src/logging.interceptor.ts

Logs HTTP request/response details when debug mode is enabled.

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private logger = new Logger('HTTP');

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (!config.debug()) {
return next.handle(); // No-op when debug disabled
}

const now = Date.now();
const { method, originalUrl } = request;

this.logger.log(`Incoming Request: ${method} ${originalUrl}`);

return next.handle().pipe(
catchError((err) => {
const delay = Date.now() - now;
this.logger.error(`Error Response: ${method} ${originalUrl} ${statusCode} - ${delay}ms`);
return throwError(() => err);
}),
tap(() => {
const delay = Date.now() - now;
this.logger.log(`Outgoing Response: ${method} ${originalUrl} ${statusCode} - ${delay}ms`);
})
);
}
}

Output Examples:

[HTTP] Incoming Request: POST /api/deploy/works/abc123
[HTTP] Outgoing Response: POST /api/deploy/works/abc123 200 - 342ms
[HTTP] Error Response: POST /api/screenshot/capture 400 - 15ms

SentryInterceptor

Source: @ever-works/monitoring

Captures unhandled exceptions and forwards them to Sentry for real-time error tracking. Configured via:

MonitoringModule.forRoot({
sentry: {
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development'
}
});

PostHogInterceptor

Source: @ever-works/monitoring

Tracks API usage events and errors in PostHog for product analytics. Configured via:

MonitoringModule.forRoot({
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST || 'https://app.posthog.com'
}
});

Custom Decorators

@Public()

Source: apps/api/src/auth/decorators/public.decorator.ts

Marks a route or controller as publicly accessible, bypassing JWT authentication.

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Usage:

@Public()
@Get('health')
healthCheck() {
return { status: 'ok' };
}

@CurrentUser()

Source: apps/api/src/auth/decorators/user.decorator.ts

Parameter decorator that extracts the authenticated user from the request.

export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
});

Usage:

@Post('deploy')
async deploy(@CurrentUser() auth: AuthenticatedUser) {
// auth.userId is available
}

The returned AuthenticatedUser object contains at minimum userId and other JWT claims.

@CrmSync()

Source: apps/api/src/integrations/twenty-crm/decorators/crm-sync.decorator.ts

Metadata decorator for marking routes that require CRM synchronization:

export const CRM_SYNC_KEY = 'crm_sync';
export const CrmSync = (enabled: boolean = true) => SetMetadata(CRM_SYNC_KEY, enabled);

Middleware Stack

Helmet

Security headers middleware applied in main.ts. Uses a relaxed Content Security Policy for the API documentation route (/api/docs) to support Scalar's inline scripts:

// Documentation routes: relaxed CSP
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:']
}
}
});

// All other routes: strict defaults
helmet();

CORS

Cross-Origin Resource Sharing configured for the web frontend:

app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
});

Body Parser

Increased payload limits for large requests:

app.use(json({ limit: '10mb' }));
app.use(urlencoded({ limit: '10mb', extended: true }));

Guard Application Patterns

Controller-Level Guard

@Controller('api/twenty-crm/companies')
@UseGuards(JwtAuthGuard)
export class CompaniesController { ... }

Method-Level Guard

@UseGuards(LocalAuthGuard)
@Post('login')
async login() { ... }

Global Guard (all routes)

{ provide: APP_GUARD, useClass: JwtAuthGuard }

Source Files

FilePurpose
apps/api/src/auth/guards/jwt-auth.guard.tsJWT authentication guard
apps/api/src/auth/guards/local-auth.guard.tsUsername/password auth guard
apps/api/src/auth/decorators/public.decorator.ts@Public() decorator
apps/api/src/auth/decorators/user.decorator.ts@CurrentUser() decorator
apps/api/src/logging.interceptor.tsHTTP logging interceptor
apps/api/src/api.module.tsGlobal guard/interceptor registration
apps/api/src/main.tsMiddleware and pipe configuration
apps/api/src/config/throttler.config.tsRate limiter configuration
apps/api/src/integrations/twenty-crm/guards/crm-sync.guard.tsCRM feature guard
apps/api/src/integrations/twenty-crm/decorators/crm-sync.decorator.tsCRM sync decorator