openwa/
├── src/
│ ├── main.ts # Application entry
│ ├── app.module.ts # Root module
│ │
│ ├── common/ # Shared code
│ │ ├── decorators/
│ │ ├── dto/
│ │ ├── exceptions/
│ │ ├── filters/
│ │ ├── guards/
│ │ ├── interceptors/
│ │ ├── interfaces/
│ │ ├── pipes/
│ │ └── utils/
│ │
│ ├── config/ # Configuration
│ │ ├── config.module.ts
│ │ └── configuration.ts
│ │
│ ├── modules/ # Feature modules
│ │ ├── session/
│ │ ├── message/
│ │ ├── webhook/
│ │ ├── contact/
│ │ ├── group/
│ │ ├── auth/
│ │ └── health/
│ │
│ ├── engine/ # WhatsApp engine
│ │ ├── engine.module.ts
│ │ ├── engine.service.ts
│ │ └── interfaces/
│ │
│ ├── queue/ # Job queues
│ │ ├── queue.module.ts
│ │ └── processors/
│ │
│ └── database/ # Database
│ ├── database.module.ts
│ ├── entities/
│ └── migrations/
│
├── test/ # Tests
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── dashboard/ # Frontend dashboard
│ ├── src/
│ └── package.json
│
├── docs/ # Documentation
├── scripts/ # Utility scripts
├── docker/ # Docker files
│
├── .github/ # GitHub config
│ └── workflows/
│
├── package.json
├── tsconfig.json
├── nest-cli.json
├── .eslintrc.js
├── .prettierrc
├── docker-compose.yml
└── README.md
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2022",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["src/*"],
"@common/*": ["src/common/*"],
"@modules/*": ["src/modules/*"],
"@config/*": ["src/config/*"]
}
}
}// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'warn',
},
};// Files: kebab-case
session.controller.ts
send-message.dto.ts
api-key.guard.ts
// Classes: PascalCase
class SessionController {}
class SendMessageDto {}
class ApiKeyGuard {}
// Interfaces: PascalCase with 'I' prefix (optional)
interface ISessionConfig {}
interface SessionConfig {} // Also acceptable
// Functions/Methods: camelCase
function createSession() {}
async sendMessage() {}
// Variables: camelCase
const sessionId = 'abc';
let messageCount = 0;
// Constants: UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const DEFAULT_TIMEOUT = 30000;
// Enums: PascalCase with PascalCase values
enum SessionStatus {
Created = 'created',
Ready = 'ready',
Disconnected = 'disconnected',
}// modules/example/example.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ExampleController } from './example.controller';
import { ExampleService } from './example.service';
import { ExampleRepository } from './example.repository';
import { Example } from './entities/example.entity';
@Module({
imports: [TypeOrmModule.forFeature([Example])],
controllers: [ExampleController],
providers: [ExampleService, ExampleRepository],
exports: [ExampleService],
})
export class ExampleModule {}// modules/example/example.controller.ts
import {
Controller,
Get,
Post,
Body,
Headers,
Param,
Delete,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ApiKeyGuard } from '@common/guards/api-key.guard';
import { ExampleService } from './example.service';
import { CreateExampleDto } from './dto/create-example.dto';
import { ExampleResponseDto } from './dto/example-response.dto';
@ApiTags('examples')
@Controller('examples')
@UseGuards(ApiKeyGuard)
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create example' })
@ApiResponse({ status: 201, type: ExampleResponseDto })
async create(
@Body() dto: CreateExampleDto,
@Headers('x-request-id') requestId?: string
): Promise<ExampleResponseDto> {
return this.exampleService.create(dto, { requestId });
}
@Get(':id')
@ApiOperation({ summary: 'Get example by ID' })
@ApiResponse({ status: 200, type: ExampleResponseDto })
async findOne(@Param('id') id: string): Promise<ExampleResponseDto> {
return this.exampleService.findOne(id);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Delete example' })
async remove(@Param('id') id: string): Promise<void> {
return this.exampleService.remove(id);
}
}// modules/example/example.service.ts
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
import { ExampleRepository } from './example.repository';
import { CreateExampleDto } from './dto/create-example.dto';
import { Example } from './entities/example.entity';
@Injectable()
export class ExampleService {
private readonly logger = new Logger(ExampleService.name);
constructor(private readonly repository: ExampleRepository) {}
async create(
dto: CreateExampleDto,
context?: { requestId?: string }
): Promise<Example> {
this.logger.log(`Creating example: ${dto.name}`, context);
const example = this.repository.create(dto);
return this.repository.save(example);
}
async findOne(id: string): Promise<Example> {
const example = await this.repository.findOne({ where: { id } });
if (!example) {
throw new NotFoundException(`Example with ID ${id} not found`);
}
return example;
}
async remove(id: string): Promise<void> {
const example = await this.findOne(id);
await this.repository.remove(example);
this.logger.log(`Deleted example: ${id}`);
}
}// modules/example/dto/create-example.dto.ts
import { IsString, IsOptional, MaxLength, IsUrl } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateExampleDto {
@ApiProperty({ description: 'Example name', example: 'My Example' })
@IsString()
@MaxLength(100)
name: string;
@ApiPropertyOptional({ description: 'Optional description' })
@IsOptional()
@IsString()
@MaxLength(500)
description?: string;
@ApiPropertyOptional({ description: 'Callback URL' })
@IsOptional()
@IsUrl({ protocols: ['https'] })
callbackUrl?: string;
}gitGraph
commit id: "initial"
branch develop
commit id: "setup"
branch feature/session-api
commit id: "session controller"
commit id: "session service"
checkout develop
merge feature/session-api
branch feature/webhook
commit id: "webhook impl"
checkout develop
merge feature/webhook
checkout main
merge develop tag: "v1.0.0"
checkout develop
branch hotfix/bug-fix
commit id: "fix bug"
checkout main
merge hotfix/bug-fix tag: "v1.0.1"
checkout develop
merge hotfix/bug-fix
main # Production-ready code
develop # Integration branch
feature/* # New features
bugfix/* # Bug fixes
hotfix/* # Production hotfixes
release/* # Release preparation
Examples:
feature/session-management
feature/webhook-retry
bugfix/qr-code-timeout
hotfix/security-patch
release/1.0.0
<type>(<scope>): <subject>
<body>
<footer>
Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation
- style: Formatting (no code change)
- refactor: Code refactoring
- test: Adding tests
- chore: Maintenance
Examples:
feat(session): add multi-session support
- Implement session manager for multiple sessions
- Add session limit configuration
- Update documentation
Closes #123
fix(webhook): handle timeout errors gracefully
Previously, webhook timeouts would crash the worker.
Now they are caught and logged properly.
Fixes #456
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] Lint passes
- [ ] Self-reviewed
## Screenshots (if applicable)
## Related Issues
Closes #test/
├── unit/ # Unit tests
│ ├── services/
│ │ ├── session.service.spec.ts
│ │ └── message.service.spec.ts
│ └── utils/
│ └── encryption.spec.ts
│
├── integration/ # Integration tests
│ ├── session.integration.spec.ts
│ └── webhook.integration.spec.ts
│
└── e2e/ # End-to-end tests
├── app.e2e-spec.ts
└── session.e2e-spec.ts
// test/unit/services/session.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { SessionService } from '@modules/session/session.service';
import { SessionRepository } from '@modules/session/session.repository';
import { EngineService } from '@engine/engine.service';
describe('SessionService', () => {
let service: SessionService;
let repository: jest.Mocked<SessionRepository>;
let engineService: jest.Mocked<EngineService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SessionService,
{
provide: SessionRepository,
useValue: {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
},
},
{
provide: EngineService,
useValue: {
createClient: jest.fn(),
destroyClient: jest.fn(),
},
},
],
}).compile();
service = module.get<SessionService>(SessionService);
repository = module.get(SessionRepository);
engineService = module.get(EngineService);
});
describe('create', () => {
it('should create a new session', async () => {
const dto = { name: 'test-session' };
const session = { id: 'uuid', ...dto, status: 'created' };
repository.create.mockReturnValue(session as any);
repository.save.mockResolvedValue(session as any);
const result = await service.create(dto);
expect(result).toEqual(session);
expect(repository.create).toHaveBeenCalledWith(dto);
expect(repository.save).toHaveBeenCalled();
});
it('should throw error if name already exists', async () => {
const dto = { name: 'existing-session' };
repository.save.mockRejectedValue({ code: '23505' }); // Unique violation
await expect(service.create(dto)).rejects.toThrow();
});
});
});// test/e2e/session.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '@/app.module';
describe('Session (e2e)', () => {
let app: INestApplication;
const apiKey = 'test-api-key';
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('POST /api/sessions', () => {
it('should create a session', () => {
return request(app.getHttpServer())
.post('/api/sessions')
.set('X-API-Key', apiKey)
.send({ name: 'e2e-test-session' })
.expect(201)
.expect((res) => {
expect(res.body.success).toBe(true);
expect(res.body.data.name).toBe('e2e-test-session');
expect(res.body.data.status).toBe('created');
});
});
it('should return 401 without API key', () => {
return request(app.getHttpServer())
.post('/api/sessions')
.send({ name: 'test' })
.expect(401);
});
});
});| Type | Minimum Coverage |
|---|---|
| Unit Tests | 80% |
| Integration Tests | 60% |
| E2E Tests | Critical paths |
/**
* Session service handles all session-related operations.
*
* @example
* ```typescript
* const session = await sessionService.create({ name: 'my-bot' });
* console.log(session.id);
* ```
*/
@Injectable()
export class SessionService {
/**
* Creates a new WhatsApp session.
*
* @param dto - Session creation parameters
* @returns The created session with QR code if applicable
* @throws {ConflictException} If session name already exists
* @throws {ServiceUnavailableException} If max sessions reached
*/
async create(dto: CreateSessionDto): Promise<Session> {
// Implementation
}
}@ApiTags('sessions')
@Controller('sessions')
export class SessionController {
@Post()
@ApiOperation({
summary: 'Create a new session',
description: 'Creates a new WhatsApp session and returns QR code for authentication',
})
@ApiBody({ type: CreateSessionDto })
@ApiResponse({
status: 201,
description: 'Session created successfully',
type: SessionResponseDto,
})
@ApiResponse({
status: 409,
description: 'Session name already exists',
})
async create(@Body() dto: CreateSessionDto): Promise<SessionResponseDto> {
// Implementation
}
}// common/exceptions/business.exception.ts
export class BusinessException extends HttpException {
constructor(
public readonly code: string,
message: string,
statusCode: HttpStatus = HttpStatus.BAD_REQUEST,
public readonly details?: Record<string, any>,
) {
super({ code, message, details }, statusCode);
}
}
// Usage
throw new BusinessException(
'SESSION_NOT_READY',
'Session is not ready to send messages',
HttpStatus.BAD_REQUEST,
{ sessionId, currentStatus: session.status }
);// common/filters/http-exception.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const errorResponse = this.formatError(exception, request);
this.logger.error(
`${request.method} ${request.url} - ${status}`,
exception instanceof Error ? exception.stack : undefined,
);
response.status(status).json(errorResponse);
}
private formatError(exception: unknown, request: Request) {
return {
success: false,
error: {
code: this.getErrorCode(exception),
message: this.getErrorMessage(exception),
details: this.getErrorDetails(exception),
},
meta: {
timestamp: new Date().toISOString(),
requestId: request.headers['x-request-id'],
path: request.url,
},
};
}
}# Required
- Node.js 20 LTS
- npm 10+
- Docker & Docker Compose
- Git
# Optional (for development)
- VS Code with extensions
- Postman or Insomnia
- pgAdmin or DBeaver# 1. Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd openwa
# 2. Install dependencies
npm install
# 3. Copy environment file
cp .env.example .env
# 4. Start infrastructure services
docker compose up -d postgres redis
# 5. Run database migrations
npm run migration:run
# 6. Start development server
npm run start:dev// .vscode/extensions.json
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker",
"prisma.prisma",
"humao.rest-client",
"bradlc.vscode-tailwindcss",
"orta.vscode-jest"
]
}// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.preferences.importModuleSpecifier": "relative",
"files.exclude": {
"**/node_modules": true,
"**/dist": true
}
}OpenWA supports multiple infrastructure configurations. Choose based on your needs:
# .env.example (Minimal Profile)
# Application
NODE_ENV=development
PORT=2785
LOG_LEVEL=debug
# Database: SQLite (zero config)
DATABASE_TYPE=sqlite
DATABASE_SQLITE_PATH=./data/openwa.db
# Storage: Local filesystem
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./media
# Cache: In-memory (no Redis needed)
CACHE_TYPE=memory
# Security
ENCRYPTION_KEY=dev-encryption-key-change-in-production
API_KEY_MASTER=dev-master-key-change-in-production
# Session
SESSION_DATA_PATH=./.wwebjs_auth
MAX_SESSIONS=3
# Engine
ENGINE_TYPE=whatsapp-web.js
PUPPETEER_HEADLESS=true
# Development
SWAGGER_ENABLED=true# .env.example (Standard Profile)
# Application
NODE_ENV=production
PORT=2785
LOG_LEVEL=info
# Database: PostgreSQL
DATABASE_TYPE=postgres
DATABASE_URL=postgresql://openwa:openwa@localhost:5432/openwa
# Storage: Local filesystem
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./media
# Cache: Redis
CACHE_TYPE=redis
REDIS_URL=redis://localhost:6379
# Security (generate with: openssl rand -base64 32)
ENCRYPTION_KEY=your-32-byte-encryption-key-here
API_KEY_MASTER=your-secure-master-key
# Session
SESSION_DATA_PATH=./.wwebjs_auth
MAX_SESSIONS=10
# Engine
ENGINE_TYPE=whatsapp-web.js
PUPPETEER_HEADLESS=true
# Development
SWAGGER_ENABLED=trueTip
For development, use the Minimal Profile with SQLite. No need to set up PostgreSQL or Redis.
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug NestJS",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug"],
"console": "integratedTerminal",
"restart": true,
"autoAttachChildProcesses": true
},
{
"name": "Debug Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "test:debug"],
"console": "integratedTerminal"
}
]
}// Use Logger from NestJS
import { Logger, Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class MyService {
private readonly logger = new Logger(MyService.name);
constructor(@Inject(REQUEST) private readonly request: Request) {}
async doSomething(id: string): Promise<void> {
// Log entry with context
const requestId = this.request?.requestId;
this.logger.log(`Processing item`, { id, requestId });
try {
await this.process(id);
this.logger.log(`Item processed successfully`, { id, requestId });
} catch (error) {
// Log error with full stack
this.logger.error(`Failed to process item`, error.stack, { id, requestId });
throw error;
}
}
}Note
Propagate X-Request-ID from controller to service and include it in all logs for easier cross-component tracing.
Use an interceptor to ensure every request has a requestId and propagate it to the response header.
// common/interceptors/request-id.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RequestIdInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const requestId = request.headers['x-request-id'] || `req_${Date.now()}`;
request.requestId = requestId;
response.setHeader('X-Request-ID', requestId);
return next.handle();
}
}// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new RequestIdInterceptor());
await app.listen(3000);
}Note
If you use REQUEST injection in a service, make sure the provider is request-scoped (@Injectable({ scope: Scope.REQUEST })) so requestId does not get mixed across requests.
// Enable verbose logging for whatsapp-web.js
const client = new Client({
puppeteer: {
headless: false, // See browser window
devtools: true, // Open DevTools automatically
},
});
// Log all events for debugging
const events = ['qr', 'ready', 'authenticated', 'disconnected', 'message'];
events.forEach(event => {
client.on(event, (...args) => {
console.log(`[WA Event: ${event}]`, JSON.stringify(args, null, 2));
});
});# Run single test file
npm test -- session.service.spec.ts
# Run tests with verbose output
npm test -- --verbose
# Check for TypeScript errors
npm run build -- --noEmit
# Lint with auto-fix
npm run lint -- --fix
# Debug database queries (TypeORM)
# Add to .env: DEBUG=typeorm:query
# View Docker logs
docker compose logs -f app// ❌ Bad: N+1 query problem
const sessions = await sessionRepo.find();
for (const session of sessions) {
session.webhooks = await webhookRepo.find({ where: { sessionId: session.id } });
}
// ✅ Good: Use relations
const sessions = await sessionRepo.find({
relations: ['webhooks'],
});
// ✅ Good: Use QueryBuilder for complex queries
const sessions = await sessionRepo
.createQueryBuilder('session')
.leftJoinAndSelect('session.webhooks', 'webhook')
.where('session.status = :status', { status: 'ready' })
.orderBy('session.createdAt', 'DESC')
.take(10)
.getMany();// Cache frequently accessed data
@Injectable()
export class SessionService {
constructor(
private readonly cache: CacheService,
private readonly repo: SessionRepository,
) {}
async getSession(id: string): Promise<Session> {
// Check cache first
const cached = await this.cache.get<Session>(`session:${id}`);
if (cached) return cached;
// Fetch from database
const session = await this.repo.findOne({ where: { id } });
// Cache for 5 minutes
await this.cache.set(`session:${id}`, session, 300);
return session;
}
async updateSession(id: string, data: Partial<Session>): Promise<Session> {
const session = await this.repo.update(id, data);
// Invalidate cache
await this.cache.del(`session:${id}`);
return session;
}
}// ❌ Bad: Sequential execution
const contact1 = await getContact('id1');
const contact2 = await getContact('id2');
const contact3 = await getContact('id3');
// ✅ Good: Parallel execution
const [contact1, contact2, contact3] = await Promise.all([
getContact('id1'),
getContact('id2'),
getContact('id3'),
]);
// ✅ Good: Batch processing with concurrency limit
import pLimit from 'p-limit';
const limit = pLimit(5); // Max 5 concurrent
const results = await Promise.all(
chatIds.map(id => limit(() => sendMessage(id, text)))
);// Session cleanup to prevent memory leaks
@Injectable()
export class SessionCleanupService {
private readonly logger = new Logger(SessionCleanupService.name);
@Cron('0 */5 * * * *') // Every 5 minutes
async cleanupInactiveSessions(): Promise<void> {
const inactiveThreshold = new Date(Date.now() - 30 * 60 * 1000); // 30 min
const inactiveSessions = await this.sessionRepo.find({
where: {
status: 'disconnected',
updatedAt: LessThan(inactiveThreshold),
},
});
for (const session of inactiveSessions) {
await this.engineService.destroy(session.id);
this.logger.log(`Cleaned up inactive session: ${session.id}`);
}
}
}## QR Code Not Generated
**Symptom:** Session stuck in 'initializing' status
**Causes & Solutions:**
1. **Chrome/Puppeteer issue**
- Ensure Chromium is installed: `which chromium`
- Check Puppeteer args: `--no-sandbox --disable-setuid-sandbox`
2. **Previous session data corrupted**
- Clear session folder: `rm -rf .wwebjs_auth/session-{id}`
3. **WhatsApp rate limit**
- Wait 5-10 minutes before retrying
## Session Disconnects Randomly
**Causes & Solutions:**
1. **Memory pressure**
- Monitor memory: `docker stats`
- Increase container memory limit
2. **Network issues**
- Check WebSocket connection stability
- Implement auto-reconnect logic
3. **WhatsApp detected automation**
- Add random delays between messages
- Avoid sending too many messages quickly## Connection Pool Exhausted
**Symptom:** "too many clients already" error
**Solution:**
```typescript
// config/typeorm.config.ts
{
type: 'postgres',
// Limit pool size
extra: {
max: 20, // Default is 10
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
},
}Symptom: "relation already exists" error
Solution:
# Check migration status
npm run migration:show
# Revert last migration
npm run migration:revert
# Regenerate migration
npm run migration:generate -- -n FixMigration
### TypeScript/NestJS Issues
```markdown
## Circular Dependency
**Symptom:** "Cannot read property 'X' of undefined"
**Solution:**
```typescript
// Use forwardRef for circular deps
@Module({
imports: [
forwardRef(() => SessionModule),
],
})
export class WebhookModule {}
// In service
constructor(
@Inject(forwardRef(() => SessionService))
private readonly sessionService: SessionService,
) {}
Symptom: "Nest can't resolve dependencies"
Solution:
- Ensure provider is exported from its module
- Check if module is imported where needed
- Use @Injectable() decorator on services
### Docker Issues
```markdown
## Container Keeps Restarting
**Check logs:**
```bash
docker compose logs app --tail 100
Common causes:
- Missing environment variables
- Database not ready (use depends_on + healthcheck)
- Port already in use
Solution:
# Add shared memory size
docker run --shm-size=2gb openwaOr in docker-compose.yml:
services:
app:
shm_size: '2gb'
## 8.12 Contributing Guide
### Getting Started
```markdown
1. Fork the repository
2. Create feature branch: `git checkout -b feature/amazing-feature`
3. Make changes following our coding standards
4. Write/update tests
5. Run linter: `npm run lint`
6. Run tests: `npm test`
7. Commit: `git commit -m 'feat(scope): add amazing feature'`
8. Push: `git push origin feature/amazing-feature`
9. Open Pull Request
- [ ] Code follows project style guide
- [ ] Tests are included and passing
- [ ] Documentation is updated
- [ ] No console.log statements
- [ ] Error handling is proper
- [ ] No hardcoded values
- [ ] Security considerations addressed
- [ ] Performance impact considered**Bug Report Template:**
- **Description:** Clear description of the bug
- **Steps to Reproduce:** Numbered steps
- **Expected Behavior:** What should happen
- **Actual Behavior:** What actually happens
- **Environment:** Node version, OS, Docker version
- **Logs:** Relevant error logs