# 03 - System Architecture ## 3.1 Architecture Overview ### High-Level Architecture ```mermaid flowchart TB subgraph Clients["Clients"] C1[External Apps] C2[Dashboard] C3[n8n/Automation] end subgraph OpenWA["OpenWA Platform"] subgraph API["API Layer"] REST[REST API
NestJS] WS[WebSocket
Real-time] SWAGGER[Swagger
Documentation] end subgraph Core["Core Services"] SM[Session
Manager] MM[Message
Manager] WH[Webhook
Manager] QM[Queue
Manager] end subgraph Engine["WhatsApp Engine"] WW[whatsapp-web.js] PP[Puppeteer] CH[Chrome/Chromium] end subgraph Storage["Storage Layer"] DB[(Database
PostgreSQL/SQLite)] REDIS[(Redis
Cache/Queue)] FS[File Storage
Media Files] end end subgraph External["External"] WA[WhatsApp
Servers] WEBHOOK[Webhook
Endpoints] end Clients --> API API --> Core Core --> Engine Core --> Storage Engine --> WA Core --> WEBHOOK ``` ### Component Interaction ```mermaid sequenceDiagram participant Client participant API as REST API participant SM as Session Manager participant Engine as WA Engine participant DB as Database participant WA as WhatsApp Client->>API: Create Session API->>SM: createSession() SM->>DB: Save session config SM->>Engine: Initialize Engine->>WA: Connect WA-->>Engine: QR Code Engine-->>SM: QR Ready SM-->>API: QR Code data API-->>Client: QR Code response ``` ## 3.2 Pluggable Architecture Philosophy OpenWA is designed with a **Pluggable Architecture** that allows infrastructure components to be swapped without changing application code. This enables flexible deployments ranging from minimal single-session bots to enterprise-scale multi-tenant platforms. ### Design Philosophy ```mermaid flowchart TB subgraph Philosophy["Core Design Principles"] P1[Program to Interfaces] P2[Dependency Injection] P3[Configuration-driven] P4[Zero Code Changes] end subgraph Benefits["Benefits"] B1[Scale Up/Down Freely] B2[Test in Isolation] B3[Swap Implementations] B4[Environment-specific Config] end P1 --> B3 P2 --> B2 P3 --> B4 P4 --> B1 ``` **Key Principles:** | Principle | Description | Example | |-----------|-------------|---------| | **Program to Interfaces** | Core code depends on abstract interfaces, not concrete implementations | `IStorageAdapter` instead of `S3Client` | | **Dependency Injection** | Adapters injected at runtime via NestJS DI container | `@Inject('STORAGE_ADAPTER')` | | **Configuration-driven** | Adapter selection via environment variables | `STORAGE_TYPE=s3` | | **Zero Code Changes** | Switch adapters without modifying application code | Change `.env`, restart | ### Adapter Categories ```mermaid flowchart LR subgraph Core["Application Core"] APP[Business Logic] end subgraph Interfaces["Adapter Interfaces"] IE[IWhatsAppEngine] ID[IDatabaseAdapter] IS[IStorageAdapter] IC[ICacheAdapter] end subgraph Implementations["Concrete Implementations"] subgraph Engine E1[whatsapp-web.js] E2[Baileys] E3[MockEngine] end subgraph Database D1[SQLite] D2[PostgreSQL] end subgraph Storage S1[LocalFS] S2[S3/MinIO] end subgraph Cache C1[In-Memory] C2[Redis] end end APP --> Interfaces IE -.-> Engine ID -.-> Database IS -.-> Storage IC -.-> Cache ``` ### Adapter Lifecycle State Machine Each adapter follows a consistent lifecycle: ```mermaid stateDiagram-v2 [*] --> Uninitialized: Create Instance Uninitialized --> Initializing: initialize() Initializing --> Ready: Success Initializing --> Failed: Error Ready --> Active: First operation Active --> Ready: Operation complete Active --> Degraded: Transient error Degraded --> Active: Auto-recover Degraded --> Failed: Max retries exceeded Ready --> Disconnecting: shutdown() Active --> Disconnecting: shutdown() Degraded --> Disconnecting: shutdown() Disconnecting --> Disconnected: Cleanup complete Failed --> Disconnecting: shutdown() Disconnected --> [*] ``` ```typescript // common/interfaces/adapter-lifecycle.interface.ts export enum AdapterState { UNINITIALIZED = 'uninitialized', INITIALIZING = 'initializing', READY = 'ready', ACTIVE = 'active', DEGRADED = 'degraded', FAILED = 'failed', DISCONNECTING = 'disconnecting', DISCONNECTED = 'disconnected', } export interface IAdapterLifecycle { /** Current adapter state */ getState(): AdapterState; /** Initialize adapter with configuration */ initialize(config: AdapterConfig): Promise; /** Check if adapter is operational */ isHealthy(): Promise; /** Graceful shutdown */ shutdown(): Promise; /** State change event emitter */ onStateChange(handler: (state: AdapterState) => void): void; } ``` ### Dependency Injection Configuration OpenWA uses NestJS Dynamic Modules for adapter injection: ```typescript // adapters/adapters.module.ts import { DynamicModule, Global, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Global() @Module({}) export class AdaptersModule { static forRoot(): DynamicModule { return { module: AdaptersModule, providers: [ // Database Adapter { provide: 'DATABASE_ADAPTER', useFactory: (config: ConfigService) => { const type = config.get('database.type', 'sqlite'); return DatabaseAdapterFactory.create(type, config); }, inject: [ConfigService], }, // Storage Adapter { provide: 'STORAGE_ADAPTER', useFactory: (config: ConfigService) => { const type = config.get('storage.type', 'local'); return StorageAdapterFactory.create(type, config); }, inject: [ConfigService], }, // Cache Adapter { provide: 'CACHE_ADAPTER', useFactory: (config: ConfigService) => { const type = config.get('cache.type', 'memory'); return CacheAdapterFactory.create(type, config); }, inject: [ConfigService], }, // Engine Adapter { provide: 'ENGINE_FACTORY', useFactory: (config: ConfigService) => { return new EngineFactory(config); }, inject: [ConfigService], }, ], exports: [ 'DATABASE_ADAPTER', 'STORAGE_ADAPTER', 'CACHE_ADAPTER', 'ENGINE_FACTORY', ], }; } } ``` ### Using Adapters in Services ```typescript // modules/message/message.service.ts @Injectable() export class MessageService { constructor( @Inject('STORAGE_ADAPTER') private readonly storage: IStorageAdapter, @Inject('CACHE_ADAPTER') private readonly cache: ICacheAdapter, ) {} async saveMediaMessage(sessionId: string, media: Buffer, filename: string) { // Storage adapter handles whether it's local FS or S3 const result = await this.storage.upload({ buffer: media, filename, folder: `sessions/${sessionId}/media`, }); // Cache adapter handles whether it's in-memory or Redis await this.cache.set(`media:${result.key}`, result.url, 3600); return result; } } ``` ### Runtime Configuration Flow ```mermaid sequenceDiagram participant Env as .env File participant Config as ConfigService participant Factory as AdapterFactory participant DI as NestJS DI Container participant Service as Application Service Note over Env: STORAGE_TYPE=s3 Env->>Config: Load configuration Config->>Factory: Get storage.type = 's3' Factory->>Factory: new S3StorageAdapter(config) Factory->>DI: Register as 'STORAGE_ADAPTER' DI->>Service: Inject IStorageAdapter Service->>Service: Use adapter (doesn't know it's S3) ``` ### Adapter Selection Matrix | Environment | Database | Storage | Cache | Engine | Use Case | |-------------|----------|---------|-------|--------|----------| | **Development** | SQLite | Local | Memory | Mock | Fast iteration, testing | | **Testing** | SQLite | Local | Memory | Mock | CI/CD, unit tests | | **Staging** | PostgreSQL | Local | Redis | whatsapp-web.js | Pre-production validation | | **Production (Small)** | SQLite | Local | Memory | whatsapp-web.js | 1-3 sessions, VPS | | **Production (Medium)** | PostgreSQL | Local | Redis | whatsapp-web.js | 5-10 sessions | | **Production (Large)** | PostgreSQL | S3/MinIO | Redis | whatsapp-web.js | 10+ sessions, HA | ### Hot-Swap Considerations > **Note:** Adapter hot-swap (changing adapter without restart) is **not supported** in v1.0. Changing adapter requires application restart. Future considerations for hot-swap: - Graceful connection draining - State migration between adapters - Zero-downtime switching ```mermaid flowchart TD A[Config Change Detected] --> B{Hot-swap Supported?} B -->|v1.0: No| C[Log Warning] C --> D[Require Restart] B -->|Future: Yes| E[Drain Connections] E --> F[Initialize New Adapter] F --> G[Migrate State] G --> H[Switch Traffic] H --> I[Shutdown Old Adapter] ``` ## 3.3 Layered Architecture ### Layered Architecture Pattern ```mermaid flowchart TB subgraph Presentation["Presentation Layer"] direction LR REST[REST Controllers] WS[WebSocket Gateways] SWAGGER[OpenAPI Docs] end subgraph Application["Application Layer"] direction LR SESS[Session Service] MSG[Message Service] WH[Webhook Service] AUTH[Auth Service] end subgraph Domain["Domain Layer"] direction LR ENT[Entities] REPO[Repository Interfaces] EVT[Domain Events] end subgraph Infrastructure["Infrastructure Layer"] direction LR DB[Database] CACHE[Redis] ENGINE[WA Engine] HTTP[HTTP Client] end Presentation --> Application Application --> Domain Application --> Infrastructure Domain --> Infrastructure ``` ## 3.4 Module Structure ### NestJS Module Organization ``` src/ ├── main.ts # Application entry point ├── app.module.ts # Root module │ ├── common/ # Shared utilities │ ├── decorators/ │ ├── filters/ │ ├── guards/ │ ├── interceptors/ │ ├── pipes/ │ └── utils/ │ ├── config/ # Configuration │ ├── config.module.ts │ ├── config.service.ts │ └── configuration.ts │ ├── modules/ │ ├── session/ # Session management │ │ ├── session.module.ts │ │ ├── session.controller.ts │ │ ├── session.service.ts │ │ ├── session.repository.ts │ │ ├── dto/ │ │ └── entities/ │ │ │ ├── message/ # Message handling │ │ ├── message.module.ts │ │ ├── message.controller.ts │ │ ├── message.service.ts │ │ └── dto/ │ │ │ ├── webhook/ # Webhook management │ │ ├── webhook.module.ts │ │ ├── webhook.controller.ts │ │ ├── webhook.service.ts │ │ └── dto/ │ │ │ ├── contact/ # Contact management │ │ ├── contact.module.ts │ │ ├── contact.controller.ts │ │ └── contact.service.ts │ │ │ ├── group/ # Group management │ │ ├── group.module.ts │ │ ├── group.controller.ts │ │ └── group.service.ts │ │ │ ├── auth/ # Authentication │ │ ├── auth.module.ts │ │ ├── auth.guard.ts │ │ └── api-key.strategy.ts │ │ │ └── health/ # Health checks │ ├── health.module.ts │ └── health.controller.ts │ ├── engine/ # WhatsApp engine wrapper │ ├── engine.module.ts │ ├── engine.service.ts │ ├── engine.factory.ts │ └── interfaces/ │ ├── queue/ # Job queue │ ├── queue.module.ts │ ├── processors/ │ └── jobs/ │ └── database/ # Database ├── database.module.ts ├── migrations/ └── seeds/ ``` ## 3.5 Core Components Design ### 3.5.1 Session Manager ```mermaid classDiagram class SessionManager { -sessions: Map~string, Session~ -repository: SessionRepository -engineFactory: EngineFactory +createSession(config): Session +getSession(id): Session +deleteSession(id): void +getAllSessions(): Session[] +restoreSessions(): void } class Session { +id: string +name: string +status: SessionStatus +engine: WhatsAppEngine +config: SessionConfig +createdAt: Date +start(): void +stop(): void +getQR(): string } class SessionStatus { <> CREATED INITIALIZING QR_READY AUTHENTICATED READY DISCONNECTED FAILED } class WhatsAppEngine { <> +initialize(): void +sendMessage(chatId, content): MessageResult +onMessage(callback): void +getContacts(): Contact[] +disconnect(): void } SessionManager --> Session Session --> SessionStatus Session --> WhatsAppEngine ``` ### 3.5.2 Message Flow ```mermaid flowchart TB subgraph Outbound["Outbound Message Flow"] A1[API Request] --> V1[Validate] V1 --> Q1[Queue Job] Q1 --> P1[Process] P1 --> E1[Engine Send] E1 --> R1[Response] end subgraph Inbound["Inbound Message Flow"] E2[Engine Event] --> P2[Process] P2 --> S2[Store] S2 --> W2[Webhook Queue] W2 --> D2[Deliver] end ``` ### 3.5.3 Webhook System ```mermaid classDiagram class WebhookManager { -webhooks: Webhook[] -httpClient: HttpService -queue: Queue +registerWebhook(config): Webhook +removeWebhook(id): void +dispatch(event): void -deliverWithRetry(webhook, payload): void } class Webhook { +id: string +url: string +events: EventType[] +secret: string +active: boolean +retryCount: number +headers: Record } class WebhookPayload { +event: EventType +timestamp: Date +sessionId: string +data: any +signature: string } class EventType { <> MESSAGE_RECEIVED MESSAGE_SENT MESSAGE_ACK SESSION_STATUS QR_CODE } WebhookManager --> Webhook WebhookManager --> WebhookPayload Webhook --> EventType ``` ## 3.6 Data Flow Diagrams ### 3.6.1 Send Message Flow ```mermaid flowchart LR subgraph Request["1. Request"] A[Client] -->|POST /messages| B[Controller] end subgraph Validation["2. Validation"] B --> C{Valid?} C -->|No| D[400 Error] C -->|Yes| E[Service] end subgraph Processing["3. Processing"] E --> F[Get Session] F --> G{Session Ready?} G -->|No| H[400 Error] G -->|Yes| I[Queue Job] end subgraph Execution["4. Execution"] I --> J[Worker] J --> K[Engine] K --> L[WhatsApp] end subgraph Response["5. Response"] L --> M[Success] M --> N[Store] N --> O[Response] end ``` ### 3.6.2 Webhook Delivery Flow ```mermaid flowchart TB A[Event Triggered] --> B[Create Payload] B --> C[Sign Payload] C --> D[Queue Delivery Job] D --> E[Worker Process] E --> F{Deliver} F -->|Success| G[Mark Delivered] F -->|Failed| H{Retry < 3?} H -->|Yes| I[Delay & Retry] I --> E H -->|No| J[Mark Failed] J --> K[Log Error] ``` ## 3.7 Technology Architecture ### 3.7.1 Runtime Environment ```mermaid flowchart TB subgraph Container["Docker Container"] subgraph Node["Node.js Runtime"] NEST[NestJS Application] WW[whatsapp-web.js] end subgraph Browser["Headless Browser"] CHROME[Chromium] end Node --> Browser end subgraph External["External Services"] PG[(PostgreSQL)] RD[(Redis)] end Container --> External ``` ### 3.7.2 Deployment Architecture ```mermaid flowchart TB subgraph Production["Production Environment"] LB[Load Balancer] --> I1[Instance 1] LB --> I2[Instance 2] LB --> I3[Instance N] I1 --> DB[(PostgreSQL)] I2 --> DB I3 --> DB I1 --> REDIS[(Redis)] I2 --> REDIS I3 --> REDIS end subgraph Storage["Shared Storage"] S3[S3/MinIO
Media Files] end I1 --> S3 I2 --> S3 I3 --> S3 ``` ## 3.8 API Architecture ### RESTful API Design ```mermaid flowchart LR subgraph Endpoints["API Endpoints"] direction TB S["/api/sessions"] M["/api/sessions/:sessionId/messages"] W["/api/sessions/:sessionId/webhooks"] C["/api/sessions/:sessionId/contacts"] G["/api/sessions/:sessionId/groups"] H["/health"] end subgraph Methods["HTTP Methods"] GET POST PUT DELETE end subgraph Format["Response Format"] JSON[JSON Response] ERR[Error Format] PAGE[Pagination] end ``` ### API Response Structure ```typescript // Success Response { "success": true, "data": { ... }, "meta": { "timestamp": "2025-02-02T10:00:00Z", "requestId": "uuid" } } // Error Response { "success": false, "error": { "code": "SESSION_NOT_FOUND", "message": "Session with id 'xxx' not found", "details": { ... } }, "meta": { "timestamp": "2025-02-02T10:00:00Z", "requestId": "uuid" } } // Paginated Response { "success": true, "data": [ ... ], "pagination": { "page": 1, "limit": 20, "total": 100, "totalPages": 5 } } ``` ## 3.9 Security Architecture ```mermaid flowchart TB subgraph External["External Request"] R[Request] end subgraph Security["Security Layers"] R --> HTTPS[HTTPS/TLS] HTTPS --> CORS[CORS Check] CORS --> RATE[Rate Limiter] RATE --> AUTH[API Key Auth] AUTH --> VAL[Input Validation] VAL --> APP[Application] end subgraph Internal["Internal Security"] APP --> ENC[Data Encryption] ENC --> LOG[Audit Logging] end ``` ## 3.10 Error Handling Architecture ```mermaid flowchart TB E[Error Occurs] --> T{Error Type} T -->|Validation| V[ValidationException] T -->|Not Found| N[NotFoundException] T -->|Auth| A[UnauthorizedException] T -->|Business| B[BusinessException] T -->|System| S[InternalException] V --> F[Exception Filter] N --> F A --> F B --> F S --> F F --> R[Formatted Response] F --> L[Log Error] L -->|Critical| AL[Alert] ``` ## 3.11 Scalability Considerations ### Horizontal Scaling Strategy ```mermaid flowchart TB subgraph Scaling["Scaling Strategy"] direction TB subgraph Stateless["Stateless Components"] API[API Servers] WORKER[Queue Workers] end subgraph Stateful["Stateful Components"] SESSION[Session Instances] end subgraph Shared["Shared State"] DB[(Database)] REDIS[(Redis)] S3[(Object Storage)] end end Stateless --> Shared Stateful --> Shared ``` ### Session Affinity ```mermaid flowchart LR subgraph Router["Request Router"] R[Request] --> H{Has Session ID?} H -->|Yes| A[Route to Affinity] H -->|No| B[Round Robin] end A --> I1[Instance 1
Session A, B] A --> I2[Instance 2
Session C, D] B --> I1 B --> I2 ``` --- ## 3.12 Engine Abstraction Layer > [!IMPORTANT] > Engine abstraction is critical to mitigate **R001: WhatsApp Protocol Changes** in Risk Management. With an abstraction layer, we can easily switch to an alternative engine (e.g., Baileys) when needed. ### Strategy Pattern for Engine ```mermaid classDiagram class IWhatsAppEngine { <> +initialize(config): Promise~void~ +connect(): Promise~void~ +disconnect(): Promise~void~ +getStatus(): EngineStatus +sendTextMessage(chatId, text): Promise~MessageResult~ +sendMediaMessage(chatId, media): Promise~MessageResult~ +getQRCode(): Promise~string~ +on(event, handler): void +off(event, handler): void } class WhatsAppWebJSEngine { -client: Client +initialize(): Promise~void~ +connect(): Promise~void~ +sendTextMessage(): Promise~MessageResult~ } class BaileysEngine { -socket: WASocket +initialize(): Promise~void~ +connect(): Promise~void~ +sendTextMessage(): Promise~MessageResult~ } class MockEngine { +initialize(): Promise~void~ +sendTextMessage(): Promise~MessageResult~ } class EngineFactory { +create(type: EngineType): IWhatsAppEngine } IWhatsAppEngine <|.. WhatsAppWebJSEngine IWhatsAppEngine <|.. BaileysEngine IWhatsAppEngine <|.. MockEngine EngineFactory --> IWhatsAppEngine ``` ### Engine Interface Definition ```typescript // engine/interfaces/whatsapp-engine.interface.ts export interface IWhatsAppEngine { // Lifecycle initialize(config: EngineConfig): Promise; connect(): Promise; disconnect(): Promise; destroy(): Promise; // Status getStatus(): EngineStatus; isReady(): boolean; // Authentication getQRCode(): Promise; getAuthState(): AuthState; // Messaging sendTextMessage(chatId: string, text: string, options?: SendOptions): Promise; sendMediaMessage(chatId: string, media: MediaInput, options?: SendOptions): Promise; sendLocationMessage(chatId: string, location: LocationInput): Promise; sendContactMessage(chatId: string, contact: ContactInput): Promise; // Contacts getContacts(): Promise; getContactById(contactId: string): Promise; getProfilePicture(contactId: string): Promise; // Groups getGroups(): Promise; getGroupById(groupId: string): Promise; createGroup(name: string, participants: string[]): Promise; // Events on(event: T, handler: EventHandler): void; off(event: T, handler: EventHandler): void; once(event: T, handler: EventHandler): void; } export type EngineStatus = 'initializing' | 'qr_ready' | 'connecting' | 'ready' | 'disconnected' | 'error'; export interface EngineConfig { sessionId: string; authStatePath?: string; puppeteerOptions?: PuppeteerOptions; proxyUrl?: string; } export type EngineEvent = | 'qr' | 'ready' | 'authenticated' | 'disconnected' | 'message' | 'message_ack' | 'message_revoke' | 'state_changed'; ``` ### Engine Factory ```typescript // engine/engine.factory.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IWhatsAppEngine } from './interfaces/whatsapp-engine.interface'; import { WhatsAppWebJSEngine } from './adapters/whatsapp-webjs.engine'; import { BaileysEngine } from './adapters/baileys.engine'; import { MockEngine } from './adapters/mock.engine'; export type EngineType = 'whatsapp-web.js' | 'baileys' | 'mock'; @Injectable() export class EngineFactory { constructor(private config: ConfigService) {} create(type?: EngineType): IWhatsAppEngine { const engineType = type || this.config.get('engine.type', 'whatsapp-web.js'); switch (engineType) { case 'whatsapp-web.js': return new WhatsAppWebJSEngine(this.config); case 'baileys': return new BaileysEngine(this.config); case 'mock': return new MockEngine(); default: throw new Error(`Unknown engine type: ${engineType}`); } } } ``` ### WhatsApp-Web.js Adapter ```typescript // engine/adapters/whatsapp-webjs.engine.ts import { Client, LocalAuth } from 'whatsapp-web.js'; import { IWhatsAppEngine, EngineConfig, EngineStatus } from '../interfaces/whatsapp-engine.interface'; export class WhatsAppWebJSEngine implements IWhatsAppEngine { private client: Client | null = null; private status: EngineStatus = 'initializing'; private eventEmitter = new EventEmitter(); async initialize(config: EngineConfig): Promise { this.client = new Client({ authStrategy: new LocalAuth({ clientId: config.sessionId, dataPath: config.authStatePath }), puppeteer: { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], ...config.puppeteerOptions, }, }); this.setupEventHandlers(); } private setupEventHandlers(): void { this.client!.on('qr', (qr) => { this.status = 'qr_ready'; this.eventEmitter.emit('qr', qr); }); this.client!.on('ready', () => { this.status = 'ready'; this.eventEmitter.emit('ready'); }); this.client!.on('disconnected', (reason) => { this.status = 'disconnected'; this.eventEmitter.emit('disconnected', reason); }); this.client!.on('message', (message) => { this.eventEmitter.emit('message', this.transformMessage(message)); }); } async connect(): Promise { this.status = 'connecting'; await this.client!.initialize(); } async disconnect(): Promise { await this.client?.logout(); this.status = 'disconnected'; } async sendTextMessage(chatId: string, text: string): Promise { const message = await this.client!.sendMessage(chatId, text); return { messageId: message.id._serialized, timestamp: new Date(message.timestamp * 1000), status: 'sent', }; } // ... other method implementations } ``` ### Baileys Adapter (Alternative Engine) ```typescript // engine/adapters/baileys.engine.ts import makeWASocket, { DisconnectReason, useMultiFileAuthState } from '@whiskeysockets/baileys'; import { IWhatsAppEngine, EngineConfig, EngineStatus } from '../interfaces/whatsapp-engine.interface'; export class BaileysEngine implements IWhatsAppEngine { private socket: ReturnType | null = null; private status: EngineStatus = 'initializing'; private eventEmitter = new EventEmitter(); async initialize(config: EngineConfig): Promise { const { state, saveCreds } = await useMultiFileAuthState( config.authStatePath || `./.baileys_auth/${config.sessionId}` ); this.socket = makeWASocket({ auth: state, printQRInTerminal: false, }); this.socket.ev.on('creds.update', saveCreds); this.setupEventHandlers(); } private setupEventHandlers(): void { this.socket!.ev.on('connection.update', (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { this.status = 'qr_ready'; this.eventEmitter.emit('qr', qr); } if (connection === 'open') { this.status = 'ready'; this.eventEmitter.emit('ready'); } if (connection === 'close') { this.status = 'disconnected'; const shouldReconnect = (lastDisconnect?.error as any)?.output?.statusCode !== DisconnectReason.loggedOut; this.eventEmitter.emit('disconnected', { shouldReconnect }); } }); this.socket!.ev.on('messages.upsert', ({ messages }) => { for (const msg of messages) { if (!msg.key.fromMe) { this.eventEmitter.emit('message', this.transformMessage(msg)); } } }); } async connect(): Promise { this.status = 'connecting'; // Baileys connects during initialize } async sendTextMessage(chatId: string, text: string): Promise { const result = await this.socket!.sendMessage(chatId, { text }); return { messageId: result!.key.id!, timestamp: new Date(), status: 'sent', }; } // ... other method implementations } ``` ### Engine Selection Configuration ```yaml # .env ENGINE_TYPE=whatsapp-web.js # Options: whatsapp-web.js, baileys, mock # For testing ENGINE_TYPE=mock ``` ### Migration Strategy ```mermaid flowchart TB subgraph Current["Current State"] A[whatsapp-web.js\nPuppeteer-based] end subgraph Risk["Risk Detection"] B{Protocol\nBreaking?} end subgraph Migration["Migration Path"] C[Update whatsapp-web.js] D[Switch to Baileys] E[Community Fork] end subgraph Resolution["Resolution"] F[Service Restored] end A --> B B -->|Minor| C --> F B -->|Major wwebjs| D --> F B -->|Major Both| E --> F ``` ### Engine Comparison | Feature | whatsapp-web.js | Baileys | |---------|-----------------|---------| | **Protocol** | Web (Puppeteer) | Native WebSocket | | **Resource Usage** | High (~500MB/session) | Low (~50MB/session) | | **Stability** | Good | Good | | **Community** | Large | Large | | **Multi-device** | ✅ | ✅ | | **QR Code** | ✅ | ✅ | | **Phone Link** | ❌ | ✅ | | **Maintenance** | Active | Active | ### Benefits of Abstraction 1. **Risk Mitigation** - Swap engines without changing application code 2. **Testing** - Use MockEngine for unit tests 3. **Flexibility** - Run different engines per environment 4. **Future-proof** - Easy to add new engine implementations 5. **A/B Testing** - Compare engine performance in production --- ## 3.13 Pluggable Adapters OpenWA uses the adapter pattern for infrastructure components that can be swapped per deployment needs. This allows users with limited resources to run OpenWA without heavyweight external dependencies. ### Adapter Overview ```mermaid flowchart TB subgraph Core["OpenWA Core"] APP[Application Logic] end subgraph Adapters["Pluggable Adapters"] subgraph Engine["WhatsApp Engine"] E1[whatsapp-web.js] E2[Baileys] E3[Mock] end subgraph Database["Database"] D1[SQLite] D2[PostgreSQL] end subgraph Storage["Media Storage"] S1[Local Filesystem] S2[S3] S3[MinIO] end subgraph Cache["Cache/Queue"] C1[In-Memory] C2[Redis] end end APP --> Engine APP --> Database APP --> Storage APP --> Cache ``` ### Adapter Options | Component | Options | Default | Notes | |-----------|---------|---------|-------| | **WhatsApp Engine** | whatsapp-web.js, Baileys, Mock | whatsapp-web.js | Mock for testing | | **Database** | SQLite, PostgreSQL | SQLite | PostgreSQL for large-scale production | | **Media Storage** | Local, S3, MinIO | Local | S3/MinIO for horizontal scaling | | **Cache/Queue** | In-Memory, Redis | In-Memory | Redis for multi-instance | ### 3.13.1 Storage Adapter The media storage abstraction enables storing media files (images, videos, documents) across different backends. #### Interface Definition ```typescript // storage/interfaces/storage-adapter.interface.ts export interface IStorageAdapter { /** * Upload file to storage */ upload(file: UploadInput): Promise; /** * Download file from storage */ download(key: string): Promise; /** * Delete file from storage */ delete(key: string): Promise; /** * Get a public/signed URL for a file */ getUrl(key: string, expiresIn?: number): Promise; /** * Check whether a file exists */ exists(key: string): Promise; } export interface UploadInput { buffer: Buffer; filename: string; mimetype: string; folder?: string; } export interface StorageResult { key: string; url: string; size: number; mimetype: string; } ``` #### Local Storage Adapter ```typescript // storage/adapters/local-storage.adapter.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as fs from 'fs/promises'; import * as path from 'path'; import { IStorageAdapter, UploadInput, StorageResult } from '../interfaces/storage-adapter.interface'; @Injectable() export class LocalStorageAdapter implements IStorageAdapter { private readonly basePath: string; private readonly baseUrl: string; constructor(private config: ConfigService) { this.basePath = config.get('storage.local.path', './media'); this.baseUrl = config.get('storage.local.baseUrl', '/media'); } async upload(input: UploadInput): Promise { const folder = input.folder || 'uploads'; const key = `${folder}/${Date.now()}-${input.filename}`; const fullPath = path.join(this.basePath, key); // Ensure directory exists await fs.mkdir(path.dirname(fullPath), { recursive: true }); // Write file await fs.writeFile(fullPath, input.buffer); return { key, url: `${this.baseUrl}/${key}`, size: input.buffer.length, mimetype: input.mimetype, }; } async download(key: string): Promise { const fullPath = path.join(this.basePath, key); return fs.readFile(fullPath); } async delete(key: string): Promise { const fullPath = path.join(this.basePath, key); await fs.unlink(fullPath).catch(() => {}); // Ignore if not exists } async getUrl(key: string): Promise { return `${this.baseUrl}/${key}`; } async exists(key: string): Promise { const fullPath = path.join(this.basePath, key); try { await fs.access(fullPath); return true; } catch { return false; } } } ``` #### S3/MinIO Storage Adapter ```typescript // storage/adapters/s3-storage.adapter.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { IStorageAdapter, UploadInput, StorageResult } from '../interfaces/storage-adapter.interface'; @Injectable() export class S3StorageAdapter implements IStorageAdapter { private readonly client: S3Client; private readonly bucket: string; constructor(private config: ConfigService) { this.bucket = config.get('storage.s3.bucket'); this.client = new S3Client({ region: config.get('storage.s3.region', 'us-east-1'), endpoint: config.get('storage.s3.endpoint'), // For MinIO credentials: { accessKeyId: config.get('storage.s3.accessKeyId'), secretAccessKey: config.get('storage.s3.secretAccessKey'), }, forcePathStyle: config.get('storage.s3.forcePathStyle', false), // true for MinIO }); } async upload(input: UploadInput): Promise { const folder = input.folder || 'uploads'; const key = `${folder}/${Date.now()}-${input.filename}`; await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: input.buffer, ContentType: input.mimetype, })); const url = await this.getUrl(key); return { key, url, size: input.buffer.length, mimetype: input.mimetype, }; } async download(key: string): Promise { const response = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key, })); return Buffer.from(await response.Body!.transformToByteArray()); } async delete(key: string): Promise { await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key, })); } async getUrl(key: string, expiresIn = 3600): Promise { const command = new GetObjectCommand({ Bucket: this.bucket, Key: key, }); return getSignedUrl(this.client, command, { expiresIn }); } async exists(key: string): Promise { try { await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key, })); return true; } catch { return false; } } } ``` #### Storage Factory ```typescript // storage/storage.factory.ts import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IStorageAdapter } from './interfaces/storage-adapter.interface'; import { LocalStorageAdapter } from './adapters/local-storage.adapter'; import { S3StorageAdapter } from './adapters/s3-storage.adapter'; export type StorageType = 'local' | 's3' | 'minio'; @Injectable() export class StorageFactory { constructor(private config: ConfigService) {} create(type?: StorageType): IStorageAdapter { const storageType = type || this.config.get('storage.type', 'local'); switch (storageType) { case 'local': return new LocalStorageAdapter(this.config); case 's3': case 'minio': return new S3StorageAdapter(this.config); default: throw new Error(`Unknown storage type: ${storageType}`); } } } ``` ### 3.13.2 Database Adapter OpenWA supports SQLite for lightweight deployments and PostgreSQL for high-volume production. #### Database Comparison | Feature | SQLite | PostgreSQL | |---------|--------|------------| | **Setup** | Zero config | Requires server | | **Concurrent writes** | Limited (1 writer) | Excellent | | **Horizontal scaling** | ❌ | ✅ | | **Table partitioning** | ❌ | ✅ | | **Memory footprint** | ~10MB | ~100MB+ | | **Backup** | Copy file | pg_dump | | **Best for** | 1-5 sessions | 5+ sessions | #### TypeORM Configuration ```typescript // config/database.config.ts import { ConfigService } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; export const getDatabaseConfig = (config: ConfigService): TypeOrmModuleOptions => { const dbType = config.get<'sqlite' | 'postgres'>('database.type', 'sqlite'); const baseConfig = { entities: [__dirname + '/../**/*.entity{.ts,.js}'], migrations: [__dirname + '/../database/migrations/*{.ts,.js}'], synchronize: false, logging: config.get('database.logging', false), }; if (dbType === 'sqlite') { return { ...baseConfig, type: 'sqlite', database: config.get('database.sqlite.path', './data/openwa.db'), // SQLite specific optimizations extra: { // Enable WAL mode for better concurrent reads PRAGMA: 'journal_mode = WAL', }, }; } // PostgreSQL return { ...baseConfig, type: 'postgres', url: config.get('database.url'), ssl: config.get('database.ssl', false) ? { rejectUnauthorized: false } : false, extra: { max: config.get('database.pool.max', 20), connectionTimeoutMillis: 5000, idleTimeoutMillis: 30000, }, }; }; ``` #### SQLite Considerations ```typescript // database/sqlite-optimizations.ts /** * SQLite-specific optimizations and limitations */ export const SQLITE_CONFIG = { // Recommendations maxConcurrentSessions: 5, maxMessagesBeforeCleanup: 100000, // Auto-cleanup settings (no partitioning available) messageRetentionDays: 30, logRetentionDays: 7, // Write queue to avoid SQLITE_BUSY enableWriteQueue: true, writeQueueConcurrency: 1, }; /** * Middleware for SQLite write serialization */ @Injectable() export class SqliteWriteQueueService { private writeQueue = new PQueue({ concurrency: 1 }); async executeWrite(operation: () => Promise): Promise { return this.writeQueue.add(operation); } } ``` #### Migration Strategy ```typescript // database/migrations/utils/database-aware-migration.ts /** * Helper for writing migrations compatible with SQLite and PostgreSQL */ export abstract class DatabaseAwareMigration { protected isPostgres(queryRunner: QueryRunner): boolean { return queryRunner.connection.options.type === 'postgres'; } protected isSqlite(queryRunner: QueryRunner): boolean { return queryRunner.connection.options.type === 'sqlite'; } /** * Generate UUID default based on database type */ protected getUuidDefault(queryRunner: QueryRunner): string { if (this.isPostgres(queryRunner)) { return 'gen_random_uuid()'; } // SQLite: UUID must be generated at the application level return ''; } /** * Get timestamp type based on database */ protected getTimestampType(queryRunner: QueryRunner): string { if (this.isPostgres(queryRunner)) { return 'TIMESTAMP WITH TIME ZONE'; } return 'DATETIME'; } } ``` ### 3.13.3 Cache Adapter For minimal deployments, in-memory cache is sufficient. For multi-instance deployments, Redis is required. ```typescript // cache/cache.factory.ts import { CacheModuleOptions } from '@nestjs/cache-manager'; import { ConfigService } from '@nestjs/config'; import { redisStore } from 'cache-manager-redis-store'; export const getCacheConfig = async ( config: ConfigService ): Promise => { const cacheType = config.get<'memory' | 'redis'>('cache.type', 'memory'); if (cacheType === 'memory') { return { ttl: config.get('cache.ttl', 300) * 1000, max: config.get('cache.max', 1000), }; } // Redis return { store: await redisStore({ url: config.get('redis.url'), ttl: config.get('cache.ttl', 300), }), }; }; ``` ### 3.13.4 Deployment Profiles OpenWA provides several deployment profiles for different needs: ```mermaid flowchart LR subgraph Minimal["🪶 Minimal Profile"] M1[SQLite] M2[Local Storage] M3[In-Memory Cache] M4[Single Session] end subgraph Standard["⚡ Standard Profile"] S1[PostgreSQL] S2[Local Storage] S3[Redis] S4[Multi Session] end subgraph Enterprise["🏢 Enterprise Profile"] E1[PostgreSQL Cluster] E2[S3/MinIO] E3[Redis Cluster] E4[Horizontal Scaling] end ``` | Profile | Database | Storage | Cache | Sessions | RAM | Use Case | |---------|----------|---------|-------|----------|-----|----------| | **Minimal** | SQLite | Local | In-Memory | 1-3 | 512MB | Personal bot, testing | | **Standard** | PostgreSQL | Local | Redis | 5-10 | 2GB | Small business | | **Enterprise** | PostgreSQL | S3/MinIO | Redis | 10+ | 4GB+ | Agency, high volume | ### Configuration Examples #### Minimal Profile (.env) ```bash # Database DATABASE_TYPE=sqlite DATABASE_SQLITE_PATH=./data/openwa.db # Storage STORAGE_TYPE=local STORAGE_LOCAL_PATH=./media # Cache (in-memory, no config needed) CACHE_TYPE=memory # Session MAX_SESSIONS=3 # No Redis needed # REDIS_URL= ``` #### Standard Profile (.env) ```bash # Database DATABASE_TYPE=postgres DATABASE_URL=postgresql://openwa:password@localhost:5432/openwa # Storage STORAGE_TYPE=local STORAGE_LOCAL_PATH=./media # Cache CACHE_TYPE=redis REDIS_URL=redis://localhost:6379 # Session MAX_SESSIONS=10 ``` #### Enterprise Profile (.env) ```bash # Database DATABASE_TYPE=postgres DATABASE_URL=postgresql://openwa:password@db-cluster:5432/openwa DATABASE_POOL_MAX=50 # Storage STORAGE_TYPE=s3 STORAGE_S3_BUCKET=openwa-media STORAGE_S3_REGION=ap-southeast-1 STORAGE_S3_ACCESS_KEY_ID=xxx STORAGE_S3_SECRET_ACCESS_KEY=xxx # For MinIO: # STORAGE_S3_ENDPOINT=http://minio:9000 # STORAGE_S3_FORCE_PATH_STYLE=true # Cache CACHE_TYPE=redis REDIS_URL=redis://redis-cluster:6379 # Session MAX_SESSIONS=50 # Scaling ENABLE_CLUSTER_MODE=true ``` ### Auto-Detection & Recommendations ```typescript // config/profile-detector.ts import { Logger } from '@nestjs/common'; interface SystemResources { totalMemoryMB: number; availableMemoryMB: number; cpuCores: number; } export function detectRecommendedProfile(resources: SystemResources): string { const logger = new Logger('ProfileDetector'); if (resources.totalMemoryMB < 1024) { logger.warn('Low memory detected. Using minimal profile.'); logger.warn('Recommendation: SQLite + Local Storage + In-Memory Cache'); return 'minimal'; } if (resources.totalMemoryMB < 4096) { logger.log('Standard resources detected.'); logger.log('Recommendation: PostgreSQL + Local Storage + Redis'); return 'standard'; } logger.log('High resources detected.'); logger.log('Recommendation: PostgreSQL + S3 + Redis with clustering'); return 'enterprise'; } ``` ---
[← 02 - Requirements Specification](./02-requirements-specification.md) · [Documentation Index](./README.md) · [Next: 04 - Security Design →](./04-security-design.md)