Instruction file imported from Yazan-Ali-01/complytude-backend (
.cursor/rules/nest-js.mdc). Copyright stays with the author.
NestJS Development Guidelines
Core patterns, conventions, and best practices for NestJS development in the Complytude project.
Related Rules:
- project-structure.mdc - Complete project navigation guide
- technology-stack.mdc - Dependencies, versions, and compatibility
- cursor-rules.mdc - How to maintain these rules
- self-improvement.mdc - When to update rules
========================================
Core Architecture Principles
========================================
architecture:
Modular Design
- Each feature lives under /src/modules/
- Use dependency injection exclusively - never instantiate services manually
- Keep module boundaries clear - avoid circular imports
- Export only needed providers from each module
Layer Separation
- Controllers: Handle HTTP request/response only - no business logic
- Services: Contain business logic and orchestrate data access
- Repositories: Handle database operations using native 'pg' driver
- DTOs: Validate and transform input/output data
Configuration
- Use
@nestjs/configwith Joi schema validation (src/config/env.schema.ts) - All environment variables must be validated on startup
- Never hardcode secrets - use .env files
HTTP & API
- Use Fastify as HTTP adapter (not Express)
- Document all endpoints with Swagger decorators
- API accessible at /docs endpoint
- Use @fastify/cookie, @fastify/multipart, @fastify/static plugins
Authentication & Security
- Use Passport JWT for authentication
- Dual-token system: identity tokens + tenant tokens
- All database queries use parameterized SQL (prevent SQL injection)
- Implement Row-Level Security (RLS) for multi-tenancy
Internationalization
- Use nestjs-i18n for all user-facing messages
- Support English (en) and Arabic (ar) locales
- Translation keys in modules:
modules/{module}/constants/i18n.constants.ts(errors/messages). Common keys incommon/constants/i18n.constants.ts
Naming Conventions
- Modules:
{feature}.module.ts - Controllers:
{feature}.controller.ts - Services:
{feature}.service.ts - DTOs:
{action}-{feature}.dto.ts - Repositories:
{feature}.repository.ts
========================================
Database Architecture
========================================
database: driver: "pg (native PostgreSQL driver) + pgvector" architecture: "Repository pattern with BaseRepository (in libs/database)" multi_tenancy: "Row-Level Security (RLS) via session context" status: "PRE-PRODUCTION - see pre-production.mdc for migration guidelines"
DatabaseService (Shared Library - @lib/database)
The DatabaseService lives in libs/database/src/database.service.ts and provides all database operations.
Import from @lib/database.
Key Methods:
query(text, params)- Execute parameterized queries (no RLS context — use for global/non-tenant data)transaction(callback)- Run operations in a transaction (no RLS context)transactionWithTenantContext({ tenantId, isTenantAdmin?, allowCrossTenantRead? }, callback)- Transaction with tenant RLS contexttransactionWithPlatformAdminContext(callback)- Transaction with platform admin context (bypasses tenant RLS)getClient()- Get PoolClient for manual control
Session Context Variables (set automatically by transaction methods):
app.tenant_id- Current tenant UUID (set by transactionWithTenantContext)app.is_tenant_admin- Whether current user is tenant admin (set by transactionWithTenantContext)app.allow_cross_tenant_read- Allow cross-tenant SELECT (set by transactionWithTenantContext)app.platform_role- Platform admin flag (set by transactionWithPlatformAdminContext)
Features:
- Connection pooling (pg.Pool)
- Parameterized queries (SQL injection prevention)
- Transaction support with automatic rollback
- Tenant context management for RLS (transaction-scoped via SET LOCAL)
- Platform admin context for system-wide operations
Repository Pattern
All repositories extend BaseRepository<TEntity, TCreate, TUpdate> from libs/database/src/base/base.repository.ts:
Required Implementations:
mapRow(row)- Convert database row to typed entitygetSelectColumns()- Define explicit column selection (no SELECT *)
Best Practices:
- Inject DatabaseService via constructor
- Use
executeQuery()for all database operations - Use
transactionWithTenantContextfor tenant-scoped data access - Use
transactionWithPlatformAdminContextfor system-wide operations - Use explicit column selection for performance
Example Repository:
@Injectable()
export class UserRepository extends BaseRepository<
User,
CreateUserRow,
UpdateUserRow
> {
constructor(databaseService: DatabaseService) {
super(databaseService, 'users');
}
protected mapRow(row: Record<string, unknown>): User {
return {
id: row.id as string,
email: row.email as string,
firstName: row.first_name as string,
lastName: row.last_name as string,
isVerified: row.is_verified as boolean,
createdAt: row.created_at as Date,
};
}
protected getSelectColumns(): string {
return 'id, email, first_name, last_name, is_verified, created_at';
}
// Custom query example
async findByEmail(email: string): Promise<User | null> {
const result = await this.executeQuery(
`SELECT ${this.getSelectColumns()} FROM users WHERE email = $1`,
[email],
);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
}
JSON/JSONB Columns
Handling JSON fields:
- Services stringify JSON before passing to repositories
- Repositories receive JSON as strings in TCreate/TUpdate types
mapRow()parses JSON strings back to objects for TEntity
Multi-Tenancy with RLS
Row-Level Security Implementation:
- Use
transactionWithTenantContext({ tenantId }, callback)for all tenant-scoped operations - Session variable
app.tenant_idis set automatically (transaction-scoped via SET LOCAL) - RLS policies use
current_tenant_id_or_null()to filter data by tenant - Use
transactionWithPlatformAdminContext(callback)for system admin operations (setsapp.platform_role) - Pass
{ isTenantAdmin: true }when the user is a tenant admin (for update/delete RLS policies) - Pass
{ allowCrossTenantRead: true }for cross-tenant reads (e.g., slug uniqueness checks)
Database Migrations (Pre-Production)
IMPORTANT: Project is in pre-production. See pre-production.mdc for migration guidelines.
Quick Summary:
- ✅ PREFERRED: Update existing migration files in
scripts/migrations/for most changes - ⚠️ EXCEPTION: Only create new migrations for large, independent features
- ❌ NO backward compatibility needed - make breaking changes freely
- 🔄 Re-run migrations: Drop/recreate database after editing migrations
========================================
Authentication & Authorization
========================================
authentication: strategy: "Dual-token JWT authentication" tokens: identity: access: "15 minutes (identityAccessToken cookie)" refresh: "14 days (identityRefreshToken cookie)" usage: "User identity verification, tenant selection, system admin operations" tenant: access: "30 minutes (tenantAccessToken cookie)" refresh: "14 days (tenantRefreshToken cookie)" usage: "Tenant-scoped API access"
Authentication Flow
Regular User Flow:
- Login → Receive identity tokens (access + refresh)
- Select tenant → Receive tenant tokens (access + refresh)
- Identity tokens remain valid (not cleared)
- Use tenant tokens for tenant-scoped operations
System Admin Flow:
- Login → Receive identity tokens
- Access system-wide endpoints immediately
- Optionally select tenant for tenant-specific operations
Endpoint Authentication
Use @AuthOptions() decorator to specify required authentication:
// Public endpoint (no auth)
@Get('public')
async publicEndpoint() { }
// Requires identity token only
@AuthOptions({ identity: true })
@Get('profile')
async getProfile(@CurrentUserIdentity() identity: AuthenticatedIdentityUser) { }
// Requires tenant token only
@AuthOptions({ tenant: true })
@Get('documents')
async listDocuments(@CurrentUserTenant() tenant: AuthenticatedTenantUser) { }
// Requires both tokens
@AuthOptions({ identity: true, tenant: true })
@Get('admin/tenant-info')
async getTenantInfo(
@CurrentUserIdentity() identity: AuthenticatedIdentityUser,
@CurrentUserTenant() tenant: AuthenticatedTenantUser,
) { }
// Permission-based access (requires tenant token)
@AuthOptions({ tenant: true })
@UseGuards(TenantPermissionsGuard)
@RequireAnyTenantPermission('documents:create')
@Post('create')
async create() { }
For detailed authentication documentation, see:
- docs/API_CONTRACTS.md - Complete authentication strategy
- docs/ARCHITECTURE.md - Authentication architecture
========================================
RBAC (Role-Based Access Control)
========================================
📖 Complete RBAC Documentation: See docs/RBAC.md for comprehensive guide
rbac: architecture: "Dual-level RBAC system" levels: tenant: "Tenant-scoped authorization (documents, team, settings)" platform: "Platform-wide authorization (tenant management, system admin)" strategy: "Permission-based with wildcard support" system_roles: "In-memory permission sets for performance" custom_roles: "Database-backed (MVP+)" sync: "Auto-sync services run on app startup (TenantRbacSyncService, PlatformRbacSyncService)"
Tenant RBAC vs Platform RBAC
| Aspect | Tenant RBAC | Platform RBAC |
|---|---|---|
| Scope | Tenant-specific operations | Platform-wide operations |
| Authentication | Tenant token (tenantAccessToken) |
Identity token (identityAccessToken) |
| Use Cases | Documents, team, settings | Tenant management, global templates, system admin |
| Guard | TenantPermissionsGuard |
PlatformPermissionsGuard |
| Decorators | @RequireAnyTenantPermission() |
@RequireAnyPlatformPermission() |
| Roles | tenant_admin, legal_counsel, member, viewer |
system_admin, support, auditor |
========================================
Entitlement System
========================================
entitlements: strategy: "Plan-based feature access with usage tracking and credit fallback" architecture: "Event-sourced with append-only ledgers and derived projections" sync: "EntitlementSyncService syncs features/plans from constants to DB on startup"
Tenant RBAC (Tenant-Scoped Authorization)
Authentication: Requires tenant token
Use Cases: Document operations, team management, tenant settings
Tenant System Roles
| Role | Key | Permissions | Description |
|---|---|---|---|
| Tenant Admin | tenant_admin |
*:* |
Full access to all tenant features |
| Legal Counsel | legal_counsel |
documents:*, contracts:*, templates:*, regulatory:query |
AI drafting, analysis, templates |
| Member | member |
documents:create, documents:read, templates:use, regulatory:query |
Basic document creation |
| Viewer | viewer |
documents:read, regulatory:query |
Read-only access |
Tenant Permission Examples
import {
RequireAllTenantPermissions,
RequireAnyTenantPermission,
} from 'src/common/decorators/tenant-permissions.decorator';
import { TenantPermissionsGuard } from 'src/common/guards/tenant-permissions.guard';
// Tenant-scoped endpoint
@AuthOptions({ tenant: true })
@UseGuards(TenantPermissionsGuard)
@RequireAnyTenantPermission('documents:create')
@Post('documents')
async createDocument(@CurrentUserTenant() user: AuthenticatedTenantUser) {
// user.tenantId and user.role available
}
// Multiple permissions (AND logic)
@AuthOptions({ tenant: true })
@UseGuards(TenantPermissionsGuard)
@RequireAllTenantPermissions('documents:read', 'documents:delete')
@Delete('documents/:id')
async deleteDocument(@CurrentUserTenant() user: AuthenticatedTenantUser) { }
// Wildcard permission
@AuthOptions({ tenant: true })
@UseGuards(TenantPermissionsGuard)
@RequireAnyTenantPermission('documents:*')
@Post('documents/bulk')
async bulkOperation(@CurrentUserTenant() user: AuthenticatedTenantUser) { }
Platform RBAC (Platform-Wide Authorization)
Authentication: Requires identity token
Use Cases: Tenant management, global templates, system administration
Platform System Roles
| Role | Key | Permissions | Description |
|---|---|---|---|
| System Admin | system_admin |
*:* |
Full access to all platform features |
| Support | support |
Read-only permissions | Customer support access |
| Auditor | auditor |
Audit-focused permissions | Audit and compliance access |
Platform Permission Examples
import {
RequireAllPlatformPermissions,
RequireAnyPlatformPermission,
} from 'src/common/decorators/platform-permissions.decorator';
import { PlatformPermissionsGuard } from 'src/common/guards/platform-permissions.guard';
// Platform-scoped endpoint
@AuthOptions({ identity: true })
@UseGuards(PlatformPermissionsGuard)
@RequireAnyPlatformPermission('tenants:create')
@Post('admin/tenants')
async createTenant(@CurrentUserIdentity() identity: AuthenticatedIdentityUser) {
// identity.platformRole available
}
// Multiple platform permissions (AND logic)
@AuthOptions({ identity: true })
@UseGuards(PlatformPermissionsGuard)
@RequireAllPlatformPermissions('tenants:read', 'users:read')
@Get('admin/tenants/:id/users')
async getTenantUsers(@CurrentUserIdentity() identity: AuthenticatedIdentityUser) { }
// Wildcard permission
@AuthOptions({ identity: true })
@UseGuards(PlatformPermissionsGuard)
@RequireAnyPlatformPermission('templates:*')
@Post('admin/templates')
async createGlobalTemplate(@CurrentUserIdentity() identity: AuthenticatedIdentityUser) { }
When to Use Which RBAC System
| Scenario | Use | Authentication | Example |
|---|---|---|---|
| User creates document | Tenant RBAC | Tenant Token | @RequireAnyTenantPermission('documents:create') |
| User manages team | Tenant RBAC | Tenant Token | @RequireAnyTenantPermission('team:manage') |
| System admin creates tenant | Platform RBAC | Identity Token | @RequireAnyPlatformPermission('tenants:create') |
| Support views all tenants | Platform RBAC | Identity Token | @RequireAnyPlatformPermission('tenants:read') |
| System admin manages global templates | Platform RBAC | Identity Token | @RequireAnyPlatformPermission('templates:manage') |
Adding New Permissions
Tenant Permissions:
- Add to
TENANT_PERMISSIONSobject intenant-permissions.constant.ts(single source of truth) - The
ALL_TENANT_PERMISSIONSarray is automatically derived from the object - Restart app -
TenantRbacSyncServicehandles database update
Platform Permissions:
- Add to
PLATFORM_PERMISSIONSobject inplatform-permissions.constant.ts(single source of truth) - The
ALL_PLATFORM_PERMISSIONSarray is automatically derived from the object - Restart app -
PlatformRbacSyncServicehandles database update
Key RBAC Files
Tenant RBAC:
src/common/decorators/tenant-permissions.decorator.ts- Tenant permission decoratorssrc/common/guards/tenant-permissions.guard.ts- Tenant permission guardsrc/common/constants/tenant-permissions.constant.ts- Tenant permissions (source of truth)src/common/constants/tenant-system-roles.constant.ts- Tenant system role permissionssrc/modules/tenant-rbac/tenant-rbac.service.ts- Tenant RBAC business logicsrc/modules/tenant-rbac/tenant-rbac-sync.service.ts- Tenant RBAC sync servicesrc/repositories/tenant-rbac/tenant-roles.repository.ts- Tenant roles data accesssrc/repositories/tenant-rbac/tenant-permissions.repository.ts- Tenant permissions data access
Platform RBAC:
src/common/decorators/platform-permissions.decorator.ts- Platform permission decoratorssrc/common/guards/platform-permissions.guard.ts- Platform permission guardsrc/common/constants/platform-permissions.constant.ts- Platform permissions (source of truth)src/common/constants/platform-system-roles.constant.ts- Platform system role permissionssrc/modules/platform-rbac/platform-rbac.service.ts- Platform RBAC business logicsrc/modules/platform-rbac/platform-rbac-sync.service.ts- Platform RBAC sync service
Shared:
src/common/utils/permission-matcher.util.ts- Wildcard matching utilities (shared)
Global Module Architecture
Both RBAC modules are Global Modules - marked with @Global() decorator.
TenantRbacModule- Tenant-scoped authorizationPlatformRbacModule- Platform-wide authorization
Why Global?
- RBAC is a cross-cutting concern like authentication
- Guards are used across many feature modules
- Eliminates the need to import RBAC modules in every feature module
- Follows NestJS best practices for authorization systems
How to Use:
- RBAC modules are imported ONCE in
AppModule - No need to import in feature modules - automatically available
- Use guards directly in any controller
// ✅ CORRECT: No RBAC module import needed
@Module({
controllers: [MyController], // Uses guards directly
})
export class MyModule {}
// ❌ WRONG: Don't import RBAC modules in feature modules
@Module({
imports: [TenantRbacModule, PlatformRbacModule], // ← Not needed! They're global
controllers: [MyController],
})
export class MyModule {}
========================================
Audit System
========================================
audit: architecture: "Two-level decorator + interceptor pattern for automatic audit logging" module: "AuditModule (src/modules/audit/) — NOT global, import where needed" decorators: controller: "@AuditResource('documents') — sets default resource type for all methods" method: "@AuditAction('create') or @AuditAction({ action, subResource?, resourceType? })" interceptor: "AuditInterceptor (src/common/interceptors/audit.interceptor.ts)"
How Audit Works
The audit system uses a two-level decorator + interceptor pattern:
@AuditResource('documents')on the controller sets the default resource type for all endpoints@AuditAction('create')on methods sets or overrides the action (optional — falls back to HTTP method mapping)AuditInterceptorruns after the handler, reads decorator metadata, and firesAuditService.log()asynchronously (fire-and-forget)AuditServicepersists toaudit_logstable viaAuditLogsRepository— errors are caught and logged, never thrown
Decorator Usage:
@Controller('documents')
@AuditResource('documents') // Controller-level: all methods default to 'documents' resource
export class DocumentsController {
@Post()
@AuditAction('create') // Simple: logs as 'documents:create'
async create() {}
@Patch('jurisdiction')
@AuditAction({ action: 'change', subResource: 'jurisdiction' }) // Structured: logs as 'documents:change_jurisdiction'
async changeJurisdiction() {}
@Post('export')
@AuditAction({ action: 'export', resourceType: 'reports' }) // Override resource: logs as 'reports:export'
async exportDocument() {}
@Get()
// No @AuditAction — interceptor derives 'documents:read' from GET method
async findAll() {}
}
What the interceptor captures:
tenantId,userId,userRolefrom the tenant JWT contextactionfrom@AuditActionor HTTP method mapping (GET→read, POST→create, PATCH/PUT→update, DELETE→delete)resourceTypefrom@AuditAction.resourceType>@AuditResource> URL path segmentresourceIdbest-effort extracted from response body (response.idorresponse.data.id)ipAddressfromX-Forwarded-Forheader orrequest.ipuserAgentfrom request headers- Both successes and errors are logged
Key Files:
src/common/decorators/audit.decorator.ts-@AuditResource()and@AuditAction()decoratorssrc/common/interceptors/audit.interceptor.ts- Intercepts requests, reads metadata, fires audit logsrc/modules/audit/audit.service.ts-log(),getAuditLogs(),getUserAuditLogs(),countAuditLogs()src/modules/audit/audit.module.ts- AuditModule (exports AuditService)src/repositories/audit/audit-logs.repository.ts- Audit data access
========================================
Background Job Processing
========================================
queue: library: "BullMQ via @nestjs/bullmq" backend: "Redis (ioredis)" shared_lib: "libs/queue/ (@lib/queue)" producers: "API application dispatches jobs" consumers: "Worker applications process jobs"
Queue Architecture
The system uses a producer-consumer pattern with BullMQ:
Producer (API):
- Import
QueueModuleand useQueueProducerServiceto dispatch jobs - Job payloads defined in
libs/queue/src/interfaces/ - Type-safe dispatch:
queueProducer.addJob('ai-processing', 'analyze-document', payload)
Consumer (Workers):
- Extend
AbstractProcessorfrom@lib/queue - Register processors with
@Processor('queue-name')decorator - Each worker app is a separate NestJS application
Queues:
ai-processing- Document analysis jobs (consumed by worker-ai)data-ingestion- Ruleset chunking + embedding (consumed by worker-ingestion)entitlement-processing- Async entitlement operations (consumed by API)
Key Files:
libs/queue/src/queue.module.ts- Queue module registrationlibs/queue/src/queue-producer.service.ts- Type-safe job dispatchlibs/queue/src/queue.constants.ts- Queue name constantslibs/queue/src/interfaces/- Job payload interfaceslibs/queue/src/abstract-processor.ts- Base processor class
Worker Application Pattern
Workers are separate NestJS applications under apps/:
apps/worker-{name}/
├── src/
│ ├── main.ts # Bootstrap with Fastify
│ ├── worker-{name}.module.ts # Root module (imports libs)
│ ├── config/ # Worker-specific config
│ ├── processors/ # BullMQ processors
│ ├── services/ # Business logic
│ └── repositories/ # Worker-specific data access
└── package.json
Workers import shared libraries (@lib/database, @lib/queue, @lib/redis, @lib/embedding) but have their own repositories and services.
========================================
Best Practices
========================================
HTTP & API
- Use Fastify with cookie and multipart support
- Use JWT auth strategy with Passport
- Document all endpoints with Swagger decorators
- Keep API documentation up-to-date
Database
- Import DatabaseService from
@lib/database - Use DatabaseService for all operations (never instantiate pg.Pool directly)
- Always use parameterized queries to prevent SQL injection
- Repositories handle data access; services handle business logic
- Use
transactionWithTenantContextfor all tenant-scoped data access (sets RLS context) - Use
transactionWithPlatformAdminContextfor system-wide operations - Use transactions for multi-step operations
- Use explicit column selection (no SELECT *)
Architecture
- Export only needed providers from each module
- Keep business logic in services, not controllers
- Use repository pattern for all data access
- Avoid circular dependencies between modules
Validation & DTOs
- Use class-validator for all input validation
- Enable whitelist: true to strip unknown properties
- Use transform: true for automatic type conversion
- Document DTOs with @ApiProperty decorators
Internationalization
- Use nestjs-i18n for all user-facing messages
- Support both English and Arabic locales
- Define translation keys in
modules/{module}/constants/i18n.constants.tsunder errors/messages
Code Quality
- Never use
anyunless absolutely necessary - Use explicit types for all function parameters and returns
- Follow naming conventions consistently
========================================
Development Workflow
========================================
Daily Development
pnpm dev # Start API development (auto-starts services + hot-reload)
pnpm dev:all # Start all apps (API + worker-ai + worker-ingestion)
pnpm start:api # Start API only with watch mode
pnpm start:worker-ai # Start AI worker with watch mode
pnpm start:worker-ingestion # Start ingestion worker with watch mode
Database Operations
pnpm db:migrate # Run database migrations
pnpm db:seed # Seed database with initial data
pnpm docker:start # Start PostgreSQL + Redis + MinIO
pnpm docker:reset # Full reset: down -v + start + migrate
Docker (Multi-environment)
pnpm docker:dev # Start with dev overlay (hot-reload in Docker)
pnpm docker:prod # Start with production overlay
pnpm docker:services # Start infrastructure services only
pnpm docker:services:tools # Start with pgAdmin
Code Quality
pnpm lint # ESLint with auto-fix
pnpm format # Prettier formatting
pnpm type-check # TypeScript validation
Testing
pnpm test # Run all tests
pnpm test:unit # Run unit tests only
pnpm test:integration # Run integration tests only
pnpm test:coverage # Run tests with coverage
========================================
Common Patterns
========================================
Module Creation
// feature.module.ts
@Module({
imports: [DatabaseModule], // Import required modules
controllers: [FeatureController],
providers: [FeatureService, FeatureRepository],
exports: [FeatureService], // Export if needed by other modules
})
export class FeatureModule {}
Controller Pattern
// feature.controller.ts
@Controller('features')
@ApiTags('features')
export class FeatureController {
constructor(private readonly featureService: FeatureService) {}
@Get()
@AuthOptions({ tenant: true })
@ApiOperation({ summary: 'List all features' })
@ApiResponse({ status: 200, type: [FeatureResponseDto] })
async findAll(@CurrentUserTenant() user: AuthenticatedTenantUser) {
return this.featureService.findAll(user.tenantId);
}
}
Service Pattern
// feature.service.ts
@Injectable()
export class FeatureService {
constructor(private readonly featureRepository: FeatureRepository) {}
async findAll(tenantId: string): Promise<Feature[]> {
return this.featureRepository.findAll({ tenantId });
}
}
DTO Pattern
// create-feature.dto.ts
export class CreateFeatureDto {
@ApiProperty({ description: 'Feature name' })
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({ description: 'Feature description', required: false })
@IsString()
@IsOptional()
description?: string;
}
========================================
Metadata
========================================
metadata: project: "Complytude Backend" version: "3.0" updated: "2026-03-04" note: > Focused on core NestJS patterns and conventions. See project-structure.mdc for project layout. See technology-stack.mdc for dependencies and versions. See docs/API_CONTRACTS.md for detailed authentication flows. See docs/ARCHITECTURE.md for RBAC architecture. See docs/RBAC.md for comprehensive RBAC guide.