openwa/
├── src/
│ ├── main.ts # Application entry
│ ├── app.module.ts # Root module
│ ├── common/ # Shared cache, security, storage, errors, utils
│ ├── config/ # Runtime config, env validation, bootstrap security, Swagger
│ ├── core/ # Hook and plugin framework
│ ├── database/ # TypeORM data sources and migrations
│ ├── engine/ # WhatsApp engine abstraction, adapters, identity mapping
│ ├── modules/ # API feature modules
│ └── plugins/ # Built-in engine and extension plugins
├── test/ # E2E smoke tests and mocks
├── dashboard/ # React/Vite dashboard
├── sdk/ # Client SDKs: go, java, javascript, php, python
├── docs/ # Documentation
├── scripts/ # Utility scripts
├── .github/workflows/ # CI and release workflows
├── package.json
├── tsconfig.json
├── nest-cli.json
├── eslint.config.mjs
├── docker-compose.yml
├── docker-compose.dev.yml
├── Dockerfile
└── README.md
// tsconfig.json (abridged — see the file for the commented rationale)
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "ES2023",
"rootDir": ".",
"outDir": "./dist",
"types": ["node", "jest"],
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"noFallthroughCasesInSwitch": true,
"strictPropertyInitialization": false,
"strictFunctionTypes": false,
"useUnknownInCatchVariables": false
},
"include": ["src", "test"],
"exclude": ["node_modules", "dist", "dashboard"]
}There are no path aliases — no baseUrl, no paths. Import with relative paths
(../common/services/logger.service), not @/…. Under TypeScript 6 the strict family defaults on,
so the three false entries above are deliberate opt-outs pending their own migrations
(strictPropertyInitialization alone flags around 260 TypeORM entity properties). types must be
listed explicitly because TypeScript 6 no longer auto-includes every @types package.
The backend uses ESLint flat config in eslint.config.mjs with type-aware TypeScript rules,
Prettier integration, and an architecture guard for controllers. HTTP controllers must call
capability services; they must not import IWhatsAppEngine or EngineRegistry, call getEngine(),
or resolve an engine via engines.require() / engines.get().
npm run lint
npm run lint:fixThe dashboard has its own package scripts:
cd dashboard
npm run lint// 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], 'data')],
controllers: [ExampleController],
providers: [ExampleService, ExampleRepository],
exports: [ExampleService],
})
export class ExampleModule {}// modules/example/example.controller.ts
import { Controller, Get, Post, Body, Headers, Param, Delete, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ExampleService } from './example.service';
import { CreateExampleDto } from './dto/create-example.dto';
import { ExampleResponseDto } from './dto/example-response.dto';
@ApiTags('examples')
@Controller('examples')
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);
}
}Controllers are protected by the global API key guard unless marked with @Public(). Keep
controllers thin: validate transport input through DTOs, delegate behavior to services, and never
resolve an engine directly from a controller. Engine-specific details belong behind capability
services and engine adapters.
A capability service reaches the live engine through EngineRegistry, the narrow port exported by
the (global) EngineModule — not through SessionService, which drives the session lifecycle
(start/stop/delete/reconnect, owned by SessionEngineLifecycle) and should only be injected by code
that actually drives it:
@Injectable()
export class ExampleService {
constructor(private readonly engines: EngineRegistry) {}
// require() throws 400 "Session is not started" by default; pass a factory to keep an
// endpoint's own documented status/message.
private getEngine(sessionId: string): IWhatsAppEngine {
return this.engines.require(sessionId);
}
}// 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 = createLogger('ExampleService');
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 #Unit tests live next to source files as *.spec.ts. E2E smoke tests live in test/.
src/
├── common/security/ssrf-guard.spec.ts
├── engine/adapters/baileys.adapter.spec.ts
├── modules/session/session.service.spec.ts
└── modules/webhook/webhook.service.spec.ts
test/
├── app.e2e-spec.ts
├── baileys-engine.e2e-spec.ts
├── serve-static.e2e-spec.ts
├── jest-e2e.json
└── setup-e2e.ts
// src/modules/session/reconnect-config.spec.ts
import { resolveReconnectConfig } from './session.service';
describe('resolveReconnectConfig', () => {
it('keeps reconnect settings finite and bounded', () => {
// Invalid maxReconnectAttempts falls back to the default: unlimited retries (the backoff
// parks at the 1h cap); an invalid baseDelay is clamped up to the 1s minimum.
expect(resolveReconnectConfig({ maxReconnectAttempts: 'bad', reconnectBaseDelay: -1 })).toEqual({
maxAttempts: Number.POSITIVE_INFINITY,
baseDelay: 1000,
});
});
});// test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('App (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('GET /api/health', () => {
it('returns health status without an API key', () => {
return request(app.getHttpServer())
.get('/api/health')
.expect(200)
.expect(res => {
expect(res.body.status).toBe('ok');
});
});
});
});Run the normal backend checks before opening a PR:
npm test -- --runInBand
npm run test:e2e -- --runInBand
npm run lintCoverage thresholds are enforced by Jest in package.json. Security-sensitive code under
src/common/security/ has stricter thresholds than the global baseline.
/**
* 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
}
}There is no custom exception base class and no global exception filter. Handlers throw NestJS's
built-in HTTP exceptions and NestJS's own BaseExceptionFilter renders them, so responses carry the
framework default shape { statusCode, message, error }.
throw new BadRequestException('Session is not started');
throw new NotFoundException(`Contact ${contactId} not found`);Where a failure mode recurs across engines, src/common/errors/ defines a named error that extends
the NestJS exception carrying the right status, so throwing it from an adapter maps to the intended
HTTP code with no filter involved:
| Error | Extends | Status |
|---|---|---|
EngineNotSupportedError |
NotImplementedException |
501 |
ChannelMediaNotSupportedError |
NotImplementedException |
501 |
EngineNotReadyError |
ConflictException |
409 |
EngineRefusedError |
ForbiddenException |
403 |
EngineTransportError |
ServiceUnavailableException |
503 |
ChatLabelsUnsupportedError |
UnprocessableEntityException |
422 |
CallNotFoundError / ChannelNotFoundError / GroupNotFoundError / MessageNotFoundError |
NotFoundException |
404 |
Add a new one only when the condition is engine-agnostic and recurs; a one-off stays an inline
throw new BadRequestException(...).
Do not introduce a response envelope. A
ResponseInterceptor/HttpExceptionFilterpair that wrapped every payload in{ success, data, meta }was written once, never registered, and has since been deleted —dashboard/src/services/api.tsreads the raw payload.
# Required
- Node.js 22 LTS
- npm 10+
- Docker & Docker Compose
- Git
# Optional (for development)
- VS Code with extensions
- Postman or Insomnia
- pgAdmin or DBeaverDependency installation patches whatsapp-web.js for the WhatsApp Web 2.3000.x message-id rename. It applies the patch with GNU
patch, falling back togit apply— so Git alone is enough, including on Windows outside Git Bash. With neither available the install still succeeds, but the whatsapp-web.js engine then fails on every send; seedocs/12-troubleshooting-faq.md.The committed lockfile resolves dependencies from the npm registry and works with npm 12's default Git-dependency block. Do not work around
EALLOWGITby changing npm or Git configuration globally; update to a current lockfile instead.
# 1. Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
# 2. Install the locked dependencies (also installs dashboard dependencies)
npm ci
# 3. Start API + dashboard in development mode
npm run devUse npm install only when intentionally changing dependencies and updating the lockfile.
On first boot the API creates data/.env.generated with a minimal SQLite/local-storage
configuration. A project-level .env is optional; real process environment variables take precedence
over .env, which takes precedence over data/.env.generated.
For a production-image local smoke test:
docker compose -f docker-compose.dev.yml up -d --buildFor production compose:
docker compose up -d
docker compose --profile postgres up -d
docker compose --profile full up -d// .vscode/extensions.json
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker",
"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:
# Application
NODE_ENV=development
PORT=2785
LOG_LEVEL=debug
# Database: SQLite (zero config)
DATABASE_TYPE=sqlite
DATABASE_NAME=./data/openwa.sqlite
DATABASE_SYNCHRONIZE=true
# Storage: Local filesystem
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./data/media
# Redis and queue disabled by default
REDIS_ENABLED=false
QUEUE_ENABLED=false
# Optional: seed a known admin key. If omitted, OpenWA generates a random key and writes data/.api-key.
API_MASTER_KEY=
# Session
SESSION_DATA_PATH=./data/sessions
# Engine: whatsapp-web.js = Chromium-based; baileys = browser-free WebSocket
ENGINE_TYPE=whatsapp-web.js
PUPPETEER_HEADLESS=true
# Swagger defaults ON outside production and OFF under NODE_ENV=production.
# Set it explicitly to force either way.
ENABLE_SWAGGER=true# Application
NODE_ENV=production
PORT=2785
LOG_LEVEL=info
# Database: PostgreSQL
DATABASE_TYPE=postgres
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USERNAME=openwa
DATABASE_PASSWORD=<set-a-strong-password>
DATABASE_NAME=openwa
DATABASE_SYNCHRONIZE=false
DATABASE_POOL_SIZE=10
# Storage: Local filesystem
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=/app/data/media
# Cache: Redis
REDIS_ENABLED=true
REDIS_HOST=redis
REDIS_PORT=6379
QUEUE_ENABLED=true
# Security
API_MASTER_KEY=<set-a-strong-initial-admin-key>
API_KEY_PEPPER=<optional-hash-pepper>
CORS_ORIGINS=https://dashboard.example.com
# Session
SESSION_DATA_PATH=/app/data/sessions
# Engine
ENGINE_TYPE=whatsapp-web.js
PUPPETEER_HEADLESS=true
PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu
ENABLE_SWAGGER=falseTip
For development, use the minimal profile with SQLite. PostgreSQL, Redis, and S3/MinIO are optional.
// .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 createLogger from the shared LoggerService, not console.*
import { Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { createLogger } from '../common/services/logger.service';
@Injectable({ scope: Scope.REQUEST })
export class MyService {
private readonly logger = createLogger('MyService');
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 without emitting (the Nest CLI has no --noEmit; call tsc directly).
# Use the root tsconfig, not tsconfig.build.json — it is the only one that also type-checks the
# colocated *.spec.ts files, which is what CI gates on.
npx tsc --noEmit -p tsconfig.json
# Lint with auto-fix
npm run lint -- --fix
# Debug database queries (TypeORM) — add to .env:
# DATABASE_LOGGING=true
# (there is no DEBUG=typeorm:query switch; both connections read DATABASE_LOGGING)
# View Docker logs (service is `openwa-api` in docker-compose.yml, `openwa` in
# docker-compose.dev.yml — there is no service named `app`)
docker compose logs -f openwa-api// ❌ 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();// CacheService exposes typed helpers; prefer those over ad hoc string keys in feature code.
@Injectable()
export class SessionStatsService {
constructor(private readonly cache: CacheService) {}
async getCachedStats(): Promise<SessionStats | null> {
return this.cache.getSessionsStats();
}
async updateCachedStats(stats: SessionStats): Promise<void> {
await this.cache.setSessionsStats(stats);
}
}// ❌ 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))));// Bound teardown so one stuck browser/socket cannot block shutdown.
@Injectable()
export class EngineTeardownService {
private readonly logger = createLogger('EngineTeardownService');
async destroyWithTimeout(sessionId: string, engine: IWhatsAppEngine): Promise<void> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('engine.destroy() timed out')), 10_000);
});
try {
await Promise.race([engine.destroy(), timeout]);
} catch (error) {
this.logger.warn(`Engine teardown failed for ${sessionId}: ${String(error)}`);
}
}
}## QR Code Not Generated
**Symptom:** Session stuck in 'initializing' status
**Causes & Solutions:**
1. **Chrome/Puppeteer issue**
- Ensure Chrome for Testing is installed: `ls /usr/local/bin/puppeteer-chrome`
- Check Puppeteer args: `--no-sandbox --disable-setuid-sandbox`
2. **Previous session data corrupted**
- Clear the stored auth/session data for the session under `data/sessions`
- For Baileys, also check `BAILEYS_AUTH_DIR` (default `./data/baileys`)
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 --name=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 openwa-api --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:
openwa-api:
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