OpenWA Documentation

Free, Open Source, Self-Hosted WhatsApp API Gateway

16,092 stars TypeScript TypeScript 17 files ยท ~218,285 tokens #api#bot#gateway#self-hosted#whatsapp#whatsapp-api#whatsapp-automation#whatsapp-bot
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy OpenWA on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name openwa -p 2785:2785 --restart always rmyndharis/openwa:latest
Configured Services & Ports
โš™๏ธ openwa
image: build locally
๐Ÿ”Œ Ports: ${BIND_HOST:-127.0.0.1}:${API_PORT:-2785}:2785
docker-compose.yml
yaml (docker-compose.yml)
# OpenWA - Local smoke / quick-start compose
#
# This builds and runs the PRODUCTION image (same Dockerfile) against a local SQLite DB
# with a bind-mounted ./data โ€” it is a single-container local smoke test, NOT a
# hot-reload development environment (there is no source mount or `start:dev`).
# DATABASE_SYNCHRONIZE=true keeps the SQLite schema zero-config for local use; the
# production compose (docker-compose.yml) never forces synchronize (it defaults to false).
#
# Quick Start: docker compose -f docker-compose.dev.yml up -d

services:
  # API Backend
  openwa:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: openwa-api
    # Container hardening โ€” same posture as production (same Dockerfile/entrypoint).
    security_opt:
      - 'no-new-privileges:true'
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - FOWNER
      - KILL # dumb-init: forward signals to the openwa process after gosu drops uid
      - SETGID
      - SETUID
    read_only: true
    tmpfs:
      - /tmp
    # Per-container PID ceiling (cgroup pids.max). 2048 fits several whatsapp-web.js sessions, which
    # each run a multi-process Chromium; Baileys (no Chromium) uses far fewer. A fork-bomb guard, not
    # an allocation โ€” raising it is free for light containers. Do NOT use -1 (drops the guard). #636
    pids_limit: ${OPENWA_PIDS_LIMIT:-2048}
    mem_limit: ${OPENWA_MEM_LIMIT:-2g}
    ports:
      # Bind to localhost by default; set BIND_HOST=0.0.0.0 in .env to reach it from another host, and
      # API_PORT to publish on a host port other than 2785.
      - '${BIND_HOST:-127.0.0.1}:${API_PORT:-2785}:2785'
    # User-facing settings read from the environment (or .env) with a sane default fallback
    # (${VAR:-default}); override any of them without editing this file. The data paths default
    # under /app/data (the ./data bind mount) so they persist out of the box โ€” override only if you
    # also mount that target. Truly container-internal values (HOME, XDG_*, PORT) stay fixed: they
    # must match the image/entrypoint. Same convention as the production docker-compose.yml.
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      # Explicit for the same reason as the production compose file: the data is UTC either way, this
      # only sets the zone the logs and the "today" stats window read in.
      - TZ=${TZ:-UTC}
      - PORT=2785
      - HOME=/tmp
      # Chromium reads its home from the passwd entry (no /home/openwa), so it needs writable, existing
      # config/cache dirs on the tmpfs or it hard-crashes at launch; the entrypoint pre-creates them. (#254)
      - XDG_CONFIG_HOME=/tmp/.config
      - XDG_CACHE_HOME=/tmp/.cache
      - LOG_LEVEL=${LOG_LEVEL:-info}
      # Restart-resilience: auto-start previously authenticated sessions on boot. The liveness
      # watchdog (#798) only covers a RUNNING container โ€” a `compose down/up` or rebuild kills every
      # engine, and without this flag sessions sit at `disconnected` until started by hand. Default
      # ON for the dev compose (single-operator box); the app-level default stays OFF.
      - AUTO_START_SESSIONS=${AUTO_START_SESSIONS:-true}
      - DATABASE_TYPE=${DATABASE_TYPE:-sqlite}
      - DATABASE_NAME=${DATABASE_NAME:-}
      - DATABASE_SYNCHRONIZE=${DATABASE_SYNCHRONIZE:-true}
      # Engine. Forwarded empty by default so the dashboard (Infrastructure > Engine) selects the
      # active engine via data/.env.generated (default whatsapp-web.js); main.ts treats a blank
      # ENGINE_TYPE as unset, so .env.generated wins. Set ENGINE_TYPE in your .env/host to pin an
      # engine (e.g. baileys) โ€” a real value flows through here and keeps top precedence.
      - ENGINE_TYPE=${ENGINE_TYPE:-}
      - SESSION_DATA_PATH=${SESSION_DATA_PATH:-/app/data/sessions}
      - PUPPETEER_HEADLESS=${PUPPETEER_HEADLESS:-true}
      - PUPPETEER_ARGS=${PUPPETEER_ARGS:---no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu}
      - PUPPETEER_PROTOCOL_TIMEOUT_MS=${PUPPETEER_PROTOCOL_TIMEOUT_MS:-}
      # Optional WhatsApp Web version override. DEFAULT (empty, "latest", or "auto"): OpenWA auto-resolves a
      # settled build from the third-party wppconnect-team/wa-version registry and pins its remote HTML
      # (fetched into the web.whatsapp.com origin without an integrity check). Set "off" to disable pinning
      # and use the first-party build served by WhatsApp, or an exact version to pin a specific build.
      - WWEBJS_WEB_VERSION=${WWEBJS_WEB_VERSION:-}
      - WWEBJS_WEB_VERSION_REMOTE_PATH=${WWEBJS_WEB_VERSION_REMOTE_PATH:-}
      # Raise whatsapp-web.js's first-boot init wait (default 30000ms) on slow boots. Empty = default.
      - WWEBJS_AUTH_TIMEOUT_MS=${WWEBJS_AUTH_TIMEOUT_MS:-}
      # Forward the plain-HTTP dashboard CSP override; unset preserves the application default.
      - CSP_UPGRADE_INSECURE_REQUESTS=${CSP_UPGRADE_INSECURE_REQUESTS:-}
      - STORAGE_TYPE=${STORAGE_TYPE:-local}
      - STORAGE_LOCAL_PATH=${STORAGE_LOCAL_PATH:-/app/data/media}
      # Install plugins into the writable, persistent data volume (the root FS is read-only). Matches
      # docker-compose.yml and the application default; set explicitly so the path stays correct even
      # if the volume moves.
      - PLUGINS_DIR=${PLUGINS_DIR:-/app/data/plugins}
      - PLUGIN_STATE_DIR=${PLUGIN_STATE_DIR:-}
      # Plugin install-from-URL redirect policy (ssrf-guard): off unless the exact string 'true'.
      - PLUGIN_DOWNLOAD_ALLOW_INSECURE_REDIRECTS=${PLUGIN_DOWNLOAD_ALLOW_INSECURE_REDIRECTS:-false}
      - WEBHOOK_TIMEOUT=${WEBHOOK_TIMEOUT:-10000}
      - WEBHOOK_RETRY_DELAY=${WEBHOOK_RETRY_DELAY:-5000}
      - WEBHOOK_DISPATCH_CONCURRENCY=${WEBHOOK_DISPATCH_CONCURRENCY:-16}
      - WEBHOOK_DISPATCH_MAX_QUEUED=${WEBHOOK_DISPATCH_MAX_QUEUED:-1000}
      # Mirrors docker-compose.yml: flags with no dashboard route, which a .env value could not
      # otherwise reach the container at all.
      - WEBHOOK_CONTACT_DETAILS=${WEBHOOK_CONTACT_DETAILS:-}
      - BAILEYS_MARK_ONLINE_ON_CONNECT=${BAILEYS_MARK_ONLINE_ON_CONNECT:-}
      - BAILEYS_SYNC_FULL_HISTORY=${BAILEYS_SYNC_FULL_HISTORY:-}
      - BAILEYS_WA_VERSION=${BAILEYS_WA_VERSION:-}
      - ALLOW_UNSIGNED_INGRESS=${ALLOW_UNSIGNED_INGRESS:-}
      - STORE_EPHEMERAL_MESSAGES=${STORE_EPHEMERAL_MESSAGES:-}
      - RESOLVE_LID_TO_PHONE=${RESOLVE_LID_TO_PHONE:-}
      - SIMULATE_TYPING=${SIMULATE_TYPING:-}
      - MCP_ENABLED=${MCP_ENABLED:-}
      - SEARCH_ENABLED=${SEARCH_ENABLED:-}
      - SERVE_DASHBOARD=${SERVE_DASHBOARD:-}
      - CACHE_ENABLED=${CACHE_ENABLED:-}
      - DATABASE_LOGGING=${DATABASE_LOGGING:-}
      - MAIN_DATABASE_SYNCHRONIZE=${MAIN_DATABASE_SYNCHRONIZE:-}
      - RATE_LIMIT_SHORT_TTL=${RATE_LIMIT_SHORT_TTL:-}
      - RATE_LIMIT_SHORT_LIMIT=${RATE_LIMIT_SHORT_LIMIT:-}
      - RATE_LIMIT_MEDIUM_TTL=${RATE_LIMIT_MEDIUM_TTL:-}
      - RATE_LIMIT_MEDIUM_LIMIT=${RATE_LIMIT_MEDIUM_LIMIT:-}
      - RATE_LIMIT_LONG_TTL=${RATE_LIMIT_LONG_TTL:-}
      - RATE_LIMIT_LONG_LIMIT=${RATE_LIMIT_LONG_LIMIT:-}
      # Max request body size. Base64 media sends ride in the JSON body, so the default is generous;
      # raise this for large documents (e.g. BODY_SIZE_LIMIT=50mb). Blank keeps the app default of 25mb.
      - BODY_SIZE_LIMIT=${BODY_SIZE_LIMIT:-}
      - TRUSTED_PROXIES=${TRUSTED_PROXIES:-}
      # Opt-in feature flags. Forwarded explicitly for the same reason as docker-compose.yml: this
      # file has no env_file, so a variable absent from this list never reaches the container and
      # the feature it gates stays off however the operator's .env is written.
      - CHAT_MEDIA_ARCHIVE_ENABLED=${CHAT_MEDIA_ARCHIVE_ENABLED:-}
      - CHAT_MEDIA_ARCHIVE_OUTBOUND=${CHAT_MEDIA_ARCHIVE_OUTBOUND:-}
      - CHAT_MEDIA_ARCHIVE_MAX_BYTES=${CHAT_MEDIA_ARCHIVE_MAX_BYTES:-}
      - CHAT_MEDIA_ARCHIVE_TTL_DAYS=${CHAT_MEDIA_ARCHIVE_TTL_DAYS:-}
      - MESSAGE_INLINE_MEDIA=${MESSAGE_INLINE_MEDIA:-}
      - CHAT_MEDIA_ORPHAN_SWEEP_INTERVAL_MS=${CHAT_MEDIA_ORPHAN_SWEEP_INTERVAL_MS:-}
      - CHAT_MEDIA_ORPHAN_GRACE_MS=${CHAT_MEDIA_ORPHAN_GRACE_MS:-}
      - SEND_PACING_ENABLED=${SEND_PACING_ENABLED:-}
      - SEND_PACING_WARMUP_SCHEDULE=${SEND_PACING_WARMUP_SCHEDULE:-}
      - SEND_PACING_COLD_DAILY_CAP=${SEND_PACING_COLD_DAILY_CAP:-}
      - SEND_PACING_BREAKER_THRESHOLD=${SEND_PACING_BREAKER_THRESHOLD:-}
      - SEND_PACING_BREAKER_COOLDOWN_MS=${SEND_PACING_BREAKER_COOLDOWN_MS:-}
      - MESSAGE_LIST_INLINE_MEDIA_BUDGET_BYTES=${MESSAGE_LIST_INLINE_MEDIA_BUDGET_BYTES:-}
      - MEDIA_DOWNLOAD_ENABLED=${MEDIA_DOWNLOAD_ENABLED:-}
      - MEDIA_DOWNLOAD_MAX_BYTES=${MEDIA_DOWNLOAD_MAX_BYTES:-}
      - MEDIA_DOWNLOAD_TIMEOUT_MS=${MEDIA_DOWNLOAD_TIMEOUT_MS:-}
      - INBOUND_MEDIA_CONCURRENCY=${INBOUND_MEDIA_CONCURRENCY:-}
      - INBOUND_MEDIA_GLOBAL_CONCURRENCY=${INBOUND_MEDIA_GLOBAL_CONCURRENCY:-}
      - SESSION_PROXY_URL_FETCH=${SESSION_PROXY_URL_FETCH:-}
      - MEDIA_CONVERSION_ENABLED=${MEDIA_CONVERSION_ENABLED:-}
      - FFMPEG_PATH=${FFMPEG_PATH:-}
      - MEDIA_CONVERSION_TIMEOUT_MS=${MEDIA_CONVERSION_TIMEOUT_MS:-}
      - MEDIA_CONVERSION_MAX_OUTPUT_BYTES=${MEDIA_CONVERSION_MAX_OUTPUT_BYTES:-}
      - MEDIA_CONVERSION_CONCURRENCY=${MEDIA_CONVERSION_CONCURRENCY:-}
      - NODE_ID=${NODE_ID:-}
      - NODE_URL=${NODE_URL:-}
      - SESSION_LEASE_TTL_MS=${SESSION_LEASE_TTL_MS:-}
      - SESSION_LEASE_HEARTBEAT_MS=${SESSION_LEASE_HEARTBEAT_MS:-}
      - SESSION_TAKEOVER_SWEEP_MS=${SESSION_TAKEOVER_SWEEP_MS:-}
      - SESSION_PROXY_TIMEOUT_MS=${SESSION_PROXY_TIMEOUT_MS:-}
      # Security / boot knobs the app reads. Like the blocks above, this list is the only path into
      # the container; compose-parity.spec.ts derives the required set from .env.example.
      - API_MASTER_KEY=${API_MASTER_KEY:-}
      - ALLOW_DEV_API_KEY=${ALLOW_DEV_API_KEY:-}
      - API_KEY_PEPPER=${API_KEY_PEPPER:-}
      - BOOTSTRAP_KEY_FILE=${BOOTSTRAP_KEY_FILE:-}
      - ENABLE_SWAGGER=${ENABLE_SWAGGER:-}
      - VALIDATION_ERROR_DETAIL=${VALIDATION_ERROR_DETAIL:-}
      - MCP_READONLY=${MCP_READONLY:-}
      - MCP_RATE_LIMIT_MAX=${MCP_RATE_LIMIT_MAX:-}
      - MCP_RATE_LIMIT_WINDOW_MS=${MCP_RATE_LIMIT_WINDOW_MS:-}
      - MCP_IP_RATE_LIMIT_MAX=${MCP_IP_RATE_LIMIT_MAX:-}
      - MCP_IP_RATE_LIMIT_WINDOW_MS=${MCP_IP_RATE_LIMIT_WINDOW_MS:-}
      - METRICS_TOKEN=${METRICS_TOKEN:-}
      - CORS_ORIGINS=${CORS_ORIGINS:-}
      - SSRF_ALLOWED_HOSTS=${SSRF_ALLOWED_HOSTS:-}
      - SSRF_DNS_TIMEOUT_MS=${SSRF_DNS_TIMEOUT_MS:-}
      - WEBHOOK_SSRF_REDIRECTS=${WEBHOOK_SSRF_REDIRECTS:-}
      # Postgres / S3 details (selection keys are forwarded above; blank lets a real host value pin).
      - DATABASE_HOST=${DATABASE_HOST:-}
      - DATABASE_PORT=${DATABASE_PORT:-}
      - DATABASE_USERNAME=${DATABASE_USERNAME:-}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD:-}
      - POSTGRES_SCHEMA=${POSTGRES_SCHEMA:-}
      - DATABASE_SSL=${DATABASE_SSL:-}
      - DATABASE_SSL_REJECT_UNAUTHORIZED=${DATABASE_SSL_REJECT_UNAUTHORIZED:-}
      - S3_ENDPOINT=${S3_ENDPOINT:-}
      - S3_BUCKET=${S3_BUCKET:-}
      - S3_KEY_PREFIX=${S3_KEY_PREFIX:-}
      - S3_REGION=${S3_REGION:-}
      - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-}
      - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-}
      - REDIS_ENABLED=${REDIS_ENABLED:-}
      - REDIS_HOST=${REDIS_HOST:-}
      - REDIS_PORT=${REDIS_PORT:-}
      - REDIS_USERNAME=${REDIS_USERNAME:-}
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
      - REDIS_CONNECT_TIMEOUT_MS=${REDIS_CONNECT_TIMEOUT_MS:-}
      - REDIS_TLS=${REDIS_TLS:-}
      - REDIS_CACHE_DB=${REDIS_CACHE_DB:-}
      - QUEUE_PREFIX=${QUEUE_PREFIX:-}
      # Fetch statuses on ready (off unless the exact string 'true', matching docker-compose.yml).
      - STATUS_SEED_ON_READY=${STATUS_SEED_ON_READY:-false}
      - AUTOMATION_MAX_PER_SESSION=${AUTOMATION_MAX_PER_SESSION:-}
      # HTTP server tuning.
      - REQUEST_TIMEOUT_MS=${REQUEST_TIMEOUT_MS:-}
      - HEADERS_TIMEOUT_MS=${HEADERS_TIMEOUT_MS:-}
      - KEEPALIVE_TIMEOUT_MS=${KEEPALIVE_TIMEOUT_MS:-}
      - INFLIGHT_BODY_BUDGET_BYTES=${INFLIGHT_BODY_BUDGET_BYTES:-}
      # Webhook delivery tuning and retention.
      - WEBHOOK_SHUTDOWN_DRAIN_MS=${WEBHOOK_SHUTDOWN_DRAIN_MS:-}
      - WEBHOOK_MEDIA_INLINE_MAX_BYTES=${WEBHOOK_MEDIA_INLINE_MAX_BYTES:-}
      - WEBHOOK_MAX_PAYLOAD_BYTES=${WEBHOOK_MAX_PAYLOAD_BYTES:-}
      - WEBHOOK_MAX_PER_SESSION=${WEBHOOK_MAX_PER_SESSION:-}
      - WEBHOOK_FAILURE_RETENTION_DAYS=${WEBHOOK_FAILURE_RETENTION_DAYS:-}
      - WEBHOOK_FAILURE_PAYLOAD_RETENTION_HOURS=${WEBHOOK_FAILURE_PAYLOAD_RETENTION_HOURS:-}
      - WEBHOOK_WORKER_CONCURRENCY=${WEBHOOK_WORKER_CONCURRENCY:-}
      - WEBHOOK_DEGRADED_SESSION_CONCURRENCY=${WEBHOOK_DEGRADED_SESSION_CONCURRENCY:-}
      # Integration Fabric ingress: fairness, retries, retention, reconciler, workers.
      - INGRESS_INSTANCE_LIMIT=${INGRESS_INSTANCE_LIMIT:-}
      - INGRESS_INSTANCE_TTL=${INGRESS_INSTANCE_TTL:-}
      - INGRESS_IP_LIMIT=${INGRESS_IP_LIMIT:-}
      - INGRESS_MAX_ATTEMPTS=${INGRESS_MAX_ATTEMPTS:-}
      - INGRESS_RETRY_DELAY_MS=${INGRESS_RETRY_DELAY_MS:-}
      - INGRESS_DEDUP_RETENTION_DAYS=${INGRESS_DEDUP_RETENTION_DAYS:-}
      - INGRESS_RETENTION_DAYS=${INGRESS_RETENTION_DAYS:-}
      - INGRESS_TIMESTAMP_TOLERANCE_SEC=${INGRESS_TIMESTAMP_TOLERANCE_SEC:-}
      - INGRESS_RECONCILE_INTERVAL_MS=${INGRESS_RECONCILE_INTERVAL_MS:-}
      - INGRESS_RECONCILE_GRACE_MS=${INGRESS_RECONCILE_GRACE_MS:-}
      - INGRESS_RECONCILE_BATCH_SIZE=${INGRESS_RECONCILE_BATCH_SIZE:-}
      - INGRESS_RECONCILE_MAX_ATTEMPTS=${INGRESS_RECONCILE_MAX_ATTEMPTS:-}
      - WEBHOOK_RECONCILE_INTERVAL_MS=${WEBHOOK_RECONCILE_INTERVAL_MS:-}
      - WEBHOOK_RECONCILE_GRACE_MS=${WEBHOOK_RECONCILE_GRACE_MS:-}
      - WEBHOOK_RECONCILE_BATCH_SIZE=${WEBHOOK_RECONCILE_BATCH_SIZE:-}
      - WEBHOOK_RECONCILE_MAX_ATTEMPTS=${WEBHOOK_RECONCILE_MAX_ATTEMPTS:-}
      - WEBHOOK_OUTBOX_RETENTION_DAYS=${WEBHOOK_OUTBOX_RETENTION_DAYS:-}
      - INGRESS_WORKER_CONCURRENCY=${INGRESS_WORKER_CONCURRENCY:-}
      # WebSocket gateway limits.
      - WS_RATE_LIMIT_FRAME_PER_SECOND=${WS_RATE_LIMIT_FRAME_PER_SECOND:-}
      - WS_RATE_LIMIT_FRAME_BURST=${WS_RATE_LIMIT_FRAME_BURST:-}
      - WS_RATE_LIMIT_HANDSHAKE_MAX=${WS_RATE_LIMIT_HANDSHAKE_MAX:-}
      - WS_RATE_LIMIT_HANDSHAKE_WINDOW_MS=${WS_RATE_LIMIT_HANDSHAKE_WINDOW_MS:-}
      - WS_MAX_SOCKETS_PER_KEY=${WS_MAX_SOCKETS_PER_KEY:-}
      # Database connection tuning.
      - DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE:-}
      - DATABASE_STATEMENT_TIMEOUT_MS=${DATABASE_STATEMENT_TIMEOUT_MS:-}
      - DATABASE_IDLE_TIMEOUT_MS=${DATABASE_IDLE_TIMEOUT_MS:-}
      - DATABASE_CONNECTION_TIMEOUT_MS=${DATABASE_CONNECTION_TIMEOUT_MS:-}
      - MAIN_DATABASE_NAME=${MAIN_DATABASE_NAME:-}
      # Engine / session tuning.
      - MAX_CONCURRENT_SESSIONS=${MAX_CONCURRENT_SESSIONS:-}
      - BAILEYS_AUTH_DIR=${BAILEYS_AUTH_DIR:-}
      - BAILEYS_BROWSER_NAME=${BAILEYS_BROWSER_NAME:-}
      - BAILEYS_LOG_LEVEL=${BAILEYS_LOG_LEVEL:-}
      - BAILEYS_MESSAGE_STORE_LIMIT=${BAILEYS_MESSAGE_STORE_LIMIT:-}
      - BAILEYS_SESSION_STORE_MAX_ENTRIES=${BAILEYS_SESSION_STORE_MAX_ENTRIES:-}
      - BAILEYS_CHAT_STATE_CACHE_MAX=${BAILEYS_CHAT_STATE_CACHE_MAX:-}
      - BAILEYS_GROUP_METADATA_CACHE_TTL_MS=${BAILEYS_GROUP_METADATA_CACHE_TTL_MS:-}
      - WWEBJS_ONBOARDING_CONTINUE_LABELS=${WWEBJS_ONBOARDING_CONTINUE_LABELS:-}
      # Default matches the image's ENV (Dockerfile); a blank forward would shadow it and make
      # puppeteer fall back to a bundled Chromium the image does not ship. compose-parity.spec.ts
      # asserts the default stays in sync with the Dockerfile value.
      - PUPPETEER_EXECUTABLE_PATH=${PUPPETEER_EXECUTABLE_PATH:-/usr/local/bin/puppeteer-chrome}
      - SHUTDOWN_DELAY_MS=${SHUTDOWN_DELAY_MS:-}
      - SIMULATE_TYPING_MAX_MS=${SIMULATE_TYPING_MAX_MS:-}
      - LID_MAPPING_CACHE_MAX=${LID_MAPPING_CACHE_MAX:-}
      # Media / storage budgets and sweeps.
      - EXPORT_INLINE_MEDIA_BUDGET_BYTES=${EXPORT_INLINE_MEDIA_BUDGET_BYTES:-}
      - CHAT_HISTORY_MEDIA_BUDGET_BYTES=${CHAT_HISTORY_MEDIA_BUDGET_BYTES:-}
      - STATUS_MEDIA_MAX_BYTES=${STATUS_MEDIA_MAX_BYTES:-}
      - STATUS_ORPHAN_SWEEP_INTERVAL_MS=${STATUS_ORPHAN_SWEEP_INTERVAL_MS:-}
      - STATUS_ORPHAN_GRACE_MS=${STATUS_ORPHAN_GRACE_MS:-}
      - STORAGE_IMPORT_MAX_BYTES=${STORAGE_IMPORT_MAX_BYTES:-}
      - STORAGE_IMPORT_MAX_ENTRIES=${STORAGE_IMPORT_MAX_ENTRIES:-}
      - STORAGE_IMPORT_MAX_TOTAL_BYTES=${STORAGE_IMPORT_MAX_TOTAL_BYTES:-}
      - STORAGE_EXPORT_TTL_MS=${STORAGE_EXPORT_TTL_MS:-}
      - STORAGE_EXPORT_SWEEP_MAX_AGE_MS=${STORAGE_EXPORT_SWEEP_MAX_AGE_MS:-}
      - S3_REPROBE_INTERVAL_MS=${S3_REPROBE_INTERVAL_MS:-}
      - MESSAGE_REAPER_INTERVAL_MS=${MESSAGE_REAPER_INTERVAL_MS:-}
      - MESSAGE_REAPER_GRACE_MS=${MESSAGE_REAPER_GRACE_MS:-}
      - MESSAGE_REAPER_BATCH_SIZE=${MESSAGE_REAPER_BATCH_SIZE:-}
      # Plugin sandbox limits and the install trust knobs.
      - PLUGIN_DOWNLOAD_MAX_BYTES=${PLUGIN_DOWNLOAD_MAX_BYTES:-}
      - PLUGIN_INSTALL_REQUIRE_PIN=${PLUGIN_INSTALL_REQUIRE_PIN:-}
      - PLUGIN_CATALOG_URL=${PLUGIN_CATALOG_URL:-}
      - PLUGIN_CAP_TIMEOUT_MS=${PLUGIN_CAP_TIMEOUT_MS:-}
      - PLUGIN_STORAGE_MAX_BYTES=${PLUGIN_STORAGE_MAX_BYTES:-}
      # The dashboard's update notice asks GitHub for the latest release; false turns that request off.
      - UPDATE_CHECK_ENABLED=${UPDATE_CHECK_ENABLED:-}
      # Misc application tuning.
      - AUDIT_RETENTION_DAYS=${AUDIT_RETENTION_DAYS:-}
      - MESSAGE_RETENTION_DAYS=${MESSAGE_RETENTION_DAYS:-}
      - BULK_MAX_CONCURRENT_BATCHES=${BULK_MAX_CONCURRENT_BATCHES:-}
      - TEMPLATE_RENDER_MAX_CHARS=${TEMPLATE_RENDER_MAX_CHARS:-}
      - STATS_CACHE_TTL_MS=${STATS_CACHE_TTL_MS:-}
      - SEARCH_PROVIDER=${SEARCH_PROVIDER:-}
      - SEARCH_LIMIT_MAX=${SEARCH_LIMIT_MAX:-}
      - LOG_FORMAT=${LOG_FORMAT:-}
      - BASE_URL=${BASE_URL:-}
      - DASHBOARD_URL=${DASHBOARD_URL:-}
    volumes:
      - ./data:/app/data
    restart: unless-stopped
    # Same teardown budget as docker-compose.yml, which has the arithmetic: Docker's 10s default would
    # SIGKILL the container while an engine is still stopping.
    stop_grace_period: 75s
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:2785/api/health/ready']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

  # The dashboard SPA is bundled into the image and served by NestJS on the same port:
  # open http://localhost:2785 โ€” there is no separate dashboard container.

networks:
  default:
    name: openwa-network

OpenWA Documentation


Documentation Map

Full Index (by number)

No Document Description
01 Project Overview Vision, goals, scope, current status
02 Requirements Specification Functional and non-functional requirements
03 System Architecture Architecture, modules, and runtime flows
04 Security Design Auth, rate limiting, and security controls
05 Database Design Entities and storage considerations
06 API Specification REST API and WebSocket protocol
07 API Collection Example requests and Postman import tips
08 Development Guidelines Coding standards and workflow
09 Testing Strategy Test types and tooling
10 DevOps & Infrastructure Docker, deployment, and environment configuration
11 Operational Runbooks Incident, maintenance, and backup runbooks
12 Troubleshooting FAQ Common issues and fixes
13 Horizontal Scaling Multi-node deployment guidance
14 Migration Guide Upgrade and data migration guidance
15 Project Roadmap Near-term and long-term roadmap
16 Risk Management Risks and mitigations
17 Dashboard Design Dashboard UX overview
18 SDK Design SDK plans and conventions
19 Plugin Architecture Extensibility concepts
20 Community Guidelines Contribution and governance
21 Glossary Terms and definitions
22 n8n Integration n8n community nodes for OpenWA
23 Community Integrations Third-party adapters built on the OpenWA API
24 MCP Integration Model Context Protocol tools and auth model
25 Integration Fabric Inbound webhook substrate for plugin integrations
26 Global Search Cross-session message search and the provider model
27 Plugin Search Providers Writing a search-provider plugin
28 Multitenancy Multi-tenant target design (draft proposal)
29 Engine Capability Matrix Per-engine capability support, gaps, and roadmap
30 Plugin Sandboxing Worker isolation, capabilities, and plugin limits
31 Session Lifecycle: Invariant Catalog The race windows the lifecycle defends, where each is defended, which spec pins it

Examples

Example Description
Session Phone-Number Pairing Link an existing WhatsApp account by phone number instead of scanning QR
Chat History Limits Understand local message history vs bounded live WhatsApp history
Webhook Signature Verification Verify signed OpenWA webhook deliveries in Node.js and Python
n8n Appointment Booking Workflow Build an appointment-booking flow with OpenWA and n8n
n8n to Discord Workflow Forward incoming WhatsApp messages to Discord with n8n's built-in nodes

Quick Start

Option A: Minimal Setup (SQLite, no Docker services)

bash
# Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA

# Install the locked dependencies (also installs dashboard dependencies)
npm ci

# Optional: .env.minimal pins database, storage, Redis/queue, session path, engine and Puppeteer
# settings over Dashboard > Infrastructure, and turns off session auto-start
# cp .env.minimal .env

# Run the API and the dashboard
npm run dev

Access:

  • Dashboard: http://localhost:2886
  • API: http://localhost:2785/api
  • Swagger: http://localhost:2785/api/docs
  • Health: http://localhost:2785/api/health

Option B: Docker (single container: API + Dashboard)

bash
# Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA

# Start services
docker compose up -d

Access (the dashboard is bundled into the API and served on the same port):

  • Dashboard: http://localhost:2785
  • API: http://localhost:2785/api
  • Swagger: http://localhost:2785/api/docs (off under this compose file's NODE_ENV=production unless ENABLE_SWAGGER=true)

API Key

OpenWA seeds a default API key on first run and writes it to:

  • data/.api-key (development)
  • /app/data/.api-key inside the API container when using Docker

With Docker, read it with docker exec openwa-api cat /app/data/.api-key. The file has no trailing newline, so copy only the key.

The startup log prints the full key only on the boot that created it; later boots show a masked prefix. By default a cryptographically
random owa_k1_... admin key is generated on first run in all environments; set
ALLOW_DEV_API_KEY=true to seed the well-known dev-admin-key for local
development only. Use an admin key to create additional keys with
POST /api/auth/api-keys (see
API Specification).

API Example

bash
# Create a session
curl -X POST http://localhost:2785/api/sessions \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-bot"}'

# Start the session
curl -X POST http://localhost:2785/api/sessions/{sessionId}/start \
  -H "X-API-Key: your-api-key"

# Get QR code (base64)
curl http://localhost:2785/api/sessions/{sessionId}/qr \
  -H "X-API-Key: your-api-key"

# Send a message
curl -X POST http://localhost:2785/api/sessions/{sessionId}/messages/send-text \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"chatId": "[email protected]", "text": "Hello from OpenWA!"}'

WebSocket Example (Socket.IO)

javascript
import { io } from 'socket.io-client';

const socket = io('http://localhost:2785/events', {
  extraHeaders: { 'X-API-Key': 'your-api-key' },
  transports: ['websocket'],
});

socket.on('connect', () => {
  socket.emit('message', {
    type: 'subscribe',
    sessionId: '550e8400-e29b-41d4-a716-446655440000',
    events: ['message.received', 'session.status'],
    requestId: 'req_001',
  });
});

socket.on('message', msg => {
  if (msg.type === 'event') {
    console.log('Event:', msg.payload.event, msg.payload.data);
  }
});

Features (Current)

Feature Status
REST API for WhatsApp Ready
WebSocket Events (Socket.IO) Ready
Multi-session Support Ready
Web Dashboard Ready
Docker Deployment Ready
Webhooks with HMAC Signature Ready
SQLite / PostgreSQL Storage Ready
API Key Authentication & Roles Ready
CIDR IP Whitelisting Ready
Rate Limiting Ready
Audit Logging Ready
Groups / Contacts / Labels API Ready
Channels / Status API Experimental (engine-limited)
Catalog / Product API Baileys only (whatsapp-web.js answers 501)
Pluggable Engine (wwebjs / Baileys) Ready (set ENGINE_TYPE)
Plugin Extension System Ready
Queue-based Webhook Retries Optional (QUEUE_ENABLED=true)

Tech Stack

Layer Technology
Runtime Node.js 22 LTS
Framework NestJS 11.x
Language TypeScript 6.x
WA Engine Pluggable (ENGINE_TYPE): whatsapp-web.js (default) or Baileys
WebSocket Socket.IO
Database SQLite (default) / PostgreSQL
ORM TypeORM
Container Docker + Docker Compose
Dashboard React + Vite + TanStack Query

Project Structure

text
OpenWA/
โ”œโ”€โ”€ src/                    # Backend source code
โ”œโ”€โ”€ dashboard/              # Frontend dashboard
โ”œโ”€โ”€ docker-compose.yml      # API (serves bundled dashboard) + optional datastores
โ”œโ”€โ”€ docker-compose.dev.yml  # Dev-only compose
โ”œโ”€โ”€ docs/                  # Project documentation
โ””โ”€โ”€ data/                   # Local runtime data (sessions, media, api key)

Contributing

See Development Guidelines for coding standards and workflow.

License

MIT License.