Files
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

339 lines
20 KiB
Bash

# =============================================================================
# OpenWA - Environment Configuration
# =============================================================================
# Copy this file to .env and customize as needed.
# This is the Single Source of Truth for all configuration.
# =============================================================================
# CORE SETTINGS
# =============================================================================
NODE_ENV=production
# Port the app binds to when run directly (bare metal / `npm run start:prod`). In the bundled Docker
# Compose the container always listens on 2785; the API_PORT below is only the HOST-published port.
PORT=2785
# Docker Compose only: the host-side port mapped to the container's 2785 (no effect on a bare-metal run).
API_PORT=2785
LOG_LEVEL=info # error | warn | info | debug
# Console output format. Default: json in production (containers, log aggregators),
# human-readable pretty otherwise. Force one with: json | pretty
# LOG_FORMAT=pretty
# Auto-start previously authenticated sessions on server boot. Recommended `true` for a SINGLE-instance
# production deployment so a crash-restart self-heals authenticated sessions; keep `false` if you run
# multiple replicas behind a scheduler (two replicas resurrecting one session risks a forced logout/ban —
# see docs/13-horizontal-scaling.md).
AUTO_START_SESSIONS=false
# 0 = unlimited. Set a positive integer to cap concurrently running/initializing sessions.
# Failed sessions still hold a slot until stopped (their engine stays resident); stop them to free capacity.
MAX_CONCURRENT_SESSIONS=0
# Graceful-shutdown drain: on SIGTERM/SIGINT the app flips readiness to 503 (so a load balancer stops
# routing) and keeps serving in-flight requests for this many ms before teardown. Default 3000 in
# production, 0 elsewhere (a dev hot-reload/Ctrl+C is not slowed). Capped at 30000. A second signal
# forces an immediate exit. In Docker, keep `stop_grace_period` >= this + your worst-case teardown.
# SHUTDOWN_DELAY_MS=3000
# Domain Configuration
DOMAIN=localhost
# Host interface the dev compose binds the API/dashboard ports to. Defaults to 127.0.0.1
# (localhost only). Set to 0.0.0.0 to reach the dev stack from another machine (e.g. a remote
# VPS) — put a TLS reverse proxy in front for anything internet-facing (the API key is sent in
# cleartext over plain HTTP).
# BIND_HOST=0.0.0.0
# Public URLs (for startup banner and external access)
# BASE_URL=https://api.yourdomain.com
# DASHBOARD_URL=https://dashboard.yourdomain.com
# CORS Configuration: comma-separated allowed origins. The wildcard "*" is allowed
# only in development; in production it is REFUSED (cross-origin browser requests are
# blocked) — set explicit origin(s) there, e.g. https://dashboard.yourdomain.com
CORS_ORIGINS=*
# SSL/TLS: terminate TLS at your own reverse proxy (nginx, Caddy, a cloud load
# balancer, or a k8s Ingress) in front of the API — see docs/12-troubleshooting-faq.md.
# Trusted reverse proxies (comma-separated IPs/CIDRs) whose X-Forwarded-For
# header is trusted to determine the real client IP for API-key IP whitelisting.
# Leave empty (default) to ignore X-Forwarded-For and use the direct socket
# address, which prevents IP spoofing. If you run behind a reverse proxy AND use
# per-key allowedIps restrictions, set this to the proxy's address/subnet, e.g.:
# TRUSTED_PROXIES=172.18.0.0/16
# =============================================================================
# ENGINE CONFIGURATION
# =============================================================================
# Which WhatsApp engine to use (plugin-based)
# Options: whatsapp-web.js, baileys
# Left unset by default so the dashboard (Infrastructure > Engine) governs the active engine via
# data/.env.generated (defaults to whatsapp-web.js). Uncomment to pin an engine from the environment;
# a value set here always wins over the dashboard selection.
# ENGINE_TYPE=whatsapp-web.js
# Engine-specific settings
SESSION_DATA_PATH=./data/sessions
PUPPETEER_HEADLESS=true
PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu
# Path to a system Chromium/Chrome binary. Leave unset to use the bundled Puppeteer browser.
# Required in the Docker image and on hosts without a bundled browser (e.g. Alpine/ARM).
# PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# ── Container resource limits (docker-compose only) ───────────────────────────
# These map to docker-compose `mem_limit` / `pids_limit` and only apply when running via the
# bundled compose files. They are read by docker-compose.yml, not by the app process.
# Override in your .env to tune for your fleet.
# Memory ceiling for the openwa-api container. whatsapp-web.js runs a full Chromium per session, so
# raise this for multi-session deployments (e.g. 4g). Baileys (no Chromium) is far lighter.
OPENWA_MEM_LIMIT=2g
# Per-container process (PID) ceiling — a fork-bomb guard, NOT an allocation (the kernel only
# rejects forks once the count is reached, so a higher limit is free for light containers).
# Default 2048 fits ~8-10 whatsapp-web.js sessions with startup-spike headroom; each Chromium is
# itself multi-process (browser + renderer + GPU + zygote + utilities) and WhatsApp Web is
# process-heavy, so the old default (512) could get a session's Chromium killed mid-spawn during
# startup — surfacing in the API as a `Code: null` launch failure (#636). Baileys is single-process
# and uses a handful of PIDs regardless. Raise for larger fleets; do NOT set -1 (drops the guard).
OPENWA_PIDS_LIMIT=2048
# Optional WhatsApp Web client version pin. Leave unset (or use "latest", "auto", or "off") to
# let whatsapp-web.js auto-select. If sessions get stuck at "authenticating" after scanning the QR,
# set this to a known-good version from https://github.com/wppconnect-team/wa-version (browse the
# html/ folder). WWEBJS_WEB_VERSION_REMOTE_PATH overrides the URL template (use {version} as the
# placeholder) if you self-host the version HTML.
# WWEBJS_WEB_VERSION=2.3000.1040641150-alpha
# Extend the initial boot/inject wait (milliseconds) before QR generation. On slow first boots
# (WSL2 or low-resource containers) the default 30000ms can expire before WhatsApp Web finishes
# loading. Raise it (e.g. 120000) if startup times out. Unset keeps the default (30000).
# WWEBJS_AUTH_TIMEOUT_MS=120000
# Baileys engine (used when ENGINE_TYPE=baileys). WebSocket client, no Chromium.
# NOTE: the Baileys engine is loaded lazily (dynamic import, only when ENGINE_TYPE=baileys), so there is no
# global Node.js version floor. Node.js 22 LTS is recommended for all deployments.
BAILEYS_AUTH_DIR=./data/baileys
# NOTE: proxy (PROXY_URL/PROXY_TYPE) is not yet supported by the baileys engine; it is ignored.
# Pull the FULL message-history archive on connect (large). Off by default: the engine still enables the
# initial sync (contacts, chats, recent messages, lid mappings) without downloading the entire history.
BAILEYS_SYNC_FULL_HISTORY=false
# Surface the Baileys library's own logs for debugging (trace|debug|info|warn|error). Silent by default.
# `trace` dumps the decoded WhatsApp wire frames to stdout (context "baileys-wire").
# BAILEYS_LOG_LEVEL=debug
# Humanise single sends: show a "typing…" indicator and pause briefly (length-scaled, jittered)
# before each text send so messages don't look instantaneous (anti-ban). ON by default — set to
# false to disable. SIMULATE_TYPING_MAX_MS caps the pause (default 5000). Does not affect bulk
# sends, which have their own delayBetweenMessages throttle.
# SIMULATE_TYPING=false
# SIMULATE_TYPING_MAX_MS=5000
# Inline @lid -> phone resolution (#263). When a sender is identified by a WhatsApp privacy id
# (@lid) instead of a phone number, attach a best-effort `senderPhone` (MSISDN digits, or null when
# the engine can't map it) to the message.received webhook + websocket payload. OFF by default —
# it adds a per-sender lookup (cached). The on-demand endpoint GET /sessions/:id/contacts/:id/phone
# works regardless of this flag.
# RESOLVE_LID_TO_PHONE=true
# Observability — Prometheus scrape endpoint at GET /api/metrics.
# Disabled by default; set a token to enable. Scrapers must send `Authorization: Bearer <token>`.
# METRICS_TOKEN=change-me-to-a-long-random-string
# Audit-log retention. Logs older than this many days are pruned daily (and once at startup).
# Default 90; set to 0 to keep audit logs forever (disable pruning).
# AUDIT_RETENTION_DAYS=90
# =============================================================================
# DATABASE
# =============================================================================
# Options: sqlite, postgres
DATABASE_TYPE=sqlite
POSTGRES_BUILTIN=false # Use built-in PostgreSQL container?
# PostgreSQL settings (ignored for sqlite, auto-configured if POSTGRES_BUILTIN=true)
DATABASE_HOST=localhost
DATABASE_PORT=5432
# PostgreSQL database name ONLY. Leave unset for SQLite (DATABASE_TYPE=sqlite) to use the
# default file path ./data/openwa.sqlite — a bare value here would become the SQLite file PATH
# and, under a read-only container rootfs, trigger a SQLITE_CANTOPEN boot-loop (#677).
# DATABASE_NAME=openwa
# PostgreSQL schema (ignored for sqlite). Default 'public' keeps the historical behavior.
# Set to a dedicated schema to isolate OpenWA's tables + migration ledger (e.g. on managed
# Postgres where you get a project schema, or to share one DB across apps). The schema must
# already exist — the built-in container creates it; for external/managed Postgres create it
# once (CREATE SCHEMA openwa;). A missing schema fails fast at migration time.
POSTGRES_SCHEMA=public
DATABASE_USERNAME=openwa
# MUST set a strong, unique value before using Postgres — no default is shipped.
# Production refuses to start if this is empty or a known placeholder when DATABASE_TYPE=postgres.
DATABASE_PASSWORD=
DATABASE_SYNCHRONIZE=false # WARNING: Set false in production! (data DB)
DATABASE_LOGGING=false
# Auth/audit (main) DB schema management. Default ON (zero-config first boot).
# Set to "false" to manage the api_keys/audit_logs schema via the bundled main-owned
# migration instead of synchronize (migrationsRun creates them at boot).
MAIN_DATABASE_SYNCHRONIZE=true
DATABASE_SSL=false # Set true for managed Postgres (Supabase, Heroku, Render, Railway)
DATABASE_SSL_REJECT_UNAUTHORIZED=true # Set false to allow self-signed certs (only when DATABASE_SSL=true)
# Postgres pool/query timeouts (ms). Defaults are conservative; set any to 0 to disable.
DATABASE_STATEMENT_TIMEOUT_MS=30000 # Aborts a single runtime query that runs longer (server-side; not applied to migrations)
DATABASE_IDLE_TIMEOUT_MS=30000 # Closes idle pooled connections after this long
DATABASE_CONNECTION_TIMEOUT_MS=10000 # Fails fast if a new connection can't be acquired in time
# =============================================================================
# REDIS / QUEUE (Phase 2)
# =============================================================================
REDIS_ENABLED=false # Enable Redis for queue and caching
REDIS_BUILTIN=false # Use built-in Redis container?
# Process webhooks/ingress through the BullMQ queue (needs a reachable Redis via REDIS_HOST/REDIS_PORT).
# Off by default (inline dispatch). In the bundled Docker Compose this is dashboard-managed
# (Infrastructure > Redis & Queue) — a host value here is not forwarded to the container.
# QUEUE_ENABLED=false
# Redis-backed caching, independent of the queue. Off by default (in-memory cache).
# CACHE_ENABLED=false
# Redis settings (auto-configured if REDIS_BUILTIN=true)
REDIS_HOST=localhost
REDIS_PORT=6379
# Fail Redis queue/cache connection attempts after this many ms.
REDIS_CONNECT_TIMEOUT_MS=5000
# REDIS_USERNAME=
# REDIS_PASSWORD=
# =============================================================================
# STORAGE
# =============================================================================
# Options: local, s3
STORAGE_TYPE=local
MINIO_BUILTIN=false # Use built-in MinIO container?
# Local storage path (if STORAGE_TYPE=local)
STORAGE_LOCAL_PATH=./data/media
# S3/MinIO settings (if STORAGE_TYPE=s3, auto-configured if MINIO_BUILTIN=true)
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=openwa
S3_REGION=us-east-1
# MUST set strong, unique values before using S3/MinIO — no defaults shipped.
# Production refuses to start if these are empty/placeholder when STORAGE_TYPE=s3 (external).
# Canonical names (what the app reads first and the dashboard writes); the legacy
# S3_ACCESS_KEY / S3_SECRET_KEY are still accepted as a fallback for older setups.
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
# =============================================================================
# WEBHOOK
# =============================================================================
WEBHOOK_TIMEOUT=10000 # Timeout in milliseconds
WEBHOOK_MAX_RETRIES=3 # Number of retry attempts
WEBHOOK_RETRY_DELAY=5000 # Delay between retries in ms
# Days to keep webhook delivery-failure records before pruning them (default 90; set <= 0 to disable).
# WEBHOOK_FAILURE_RETENTION_DAYS=90
# Block outbound WEBHOOK deliveries to internal/reserved addresses (SSRF
# protection). ON by default; set to "false" only on closed networks. When on,
# webhook URLs resolving to loopback/private/link-local/metadata ranges are
# refused (at registration AND delivery) and redirects are not followed.
# (Server-side media-by-URL fetches are ALWAYS SSRF-guarded, regardless of this flag.)
WEBHOOK_SSRF_PROTECT=true
# Expose the FULL sender `contact` field set (id, number, shortName, business flags, isBlocked,
# labels, …) on the message.received webhook + websocket payload. OFF by default — the payload keeps
# the minimal { name, pushName }. Opt in only if your consumer needs the richer data; all fields are
# read from the already-cached contact (no extra WhatsApp API calls).
# WEBHOOK_CONTACT_DETAILS=true
# Comma-separated hosts/IPs allowed to bypass SSRF protection for BOTH webhooks
# and media — escape-hatch for trusted internal targets (e.g. a localhost media
# store or a sidecar webhook receiver).
# SSRF_ALLOWED_HOSTS=localhost,minio
# Server-side media size/time limits:
# MEDIA_DOWNLOAD_ENABLED=true # Set to false to skip downloading inbound media entirely
# # (no decryption, no memory allocation, no storage).
# # NOTE: disabling also makes hasMedia report false and removes the
# # media field from webhooks and the dashboard.
# STORE_EPHEMERAL_MESSAGES=true # Set to false to skip persisting and dispatching incoming
# # WhatsApp disappearing messages (ephemeralDuration > 0).
# # Default: true (backward compatible — store everything).
# MEDIA_DOWNLOAD_MAX_BYTES=52428800 # cap remote-URL sends, inbound media, AND outbound base64 sends (default 50 MiB; oversized inbound media is dropped, message kept; oversized base64 is rejected with 400)
# MEDIA_DOWNLOAD_TIMEOUT_MS=30000 # abort a slow media download (default 30s)
# Storage import/export limits (ADMIN /infra/storage/* endpoints):
# STORAGE_IMPORT_MAX_BYTES=209715200 # per-entry cap for a tar.gz import; aborts on overflow (default 200 MiB)
# STORAGE_IMPORT_MAX_ENTRIES=100000 # max entries in an import archive; aborts beyond this (default 100000)
# STORAGE_EXPORT_TTL_MS=3600000 # auto-delete an export archive after this long (default 1h)
# =============================================================================
# RATE LIMITING (all TTLs are in MILLISECONDS)
# =============================================================================
# The "medium" tier is the one enforced on the API; short/long are optional extra tiers.
RATE_LIMIT_MEDIUM_TTL=60000 # window in ms (default 60000 = 60s)
RATE_LIMIT_MEDIUM_LIMIT=100 # max requests per window
# RATE_LIMIT_SHORT_TTL=1000 # 1s burst window (default)
# RATE_LIMIT_SHORT_LIMIT=10
# RATE_LIMIT_LONG_TTL=3600000 # 1h window (default)
# RATE_LIMIT_LONG_LIMIT=1000
# Per-instance fairness cap on the Integration Fabric ingress route (POST /api/ingress/:pluginId/:instanceId/*).
# Providers deliver every tenant's webhooks from one shared egress IP, so this is keyed on
# (pluginId, instanceId) instead of IP — a noisy tenant gets 429'd without throttling its neighbors.
# Independent of, and in addition to, the IP-keyed tiers above.
# INGRESS_INSTANCE_LIMIT=120 # max requests per instance per window (default 120)
# INGRESS_INSTANCE_TTL=60000 # window in ms (default 60000 = 60s)
# =============================================================================
# MCP (Model Context Protocol) — Agent / AI-assistant tool server
# =============================================================================
# Off by default. When enabled, mounts a stateless Streamable-HTTP MCP server
# at POST /mcp on the same port. See docs/24-mcp-integration.md for details.
# MCP_ENABLED=true
# Mount read-only tools only (no sends, no mutations). Recommended for observer agents.
# MCP_READONLY=true
# Per-key sliding-window rate limit for tool calls.
# Any blank/non-positive/non-numeric value falls back to the default.
# MCP_RATE_LIMIT_MAX=60 # max tool calls per key per window (default 60)
# MCP_RATE_LIMIT_WINDOW_MS=60000 # window size in ms (default 60000 = 1 min)
# Pre-auth, per-IP sliding-window throttle for the /mcp mount (gates invalid-key requests before
# key validation, since the raw mount bypasses the global throttler). Same fallback rules.
# MCP_IP_RATE_LIMIT_MAX=120 # max requests per IP per window (default 120)
# MCP_IP_RATE_LIMIT_WINDOW_MS=60000 # window size in ms (default 60000 = 1 min)
# =============================================================================
# GLOBAL MESSAGE SEARCH
# =============================================================================
# Global message search (optional). Default ON, using the built-in DB full-text
# provider (Postgres tsvector/GIN, SQLite FTS5) — zero external services.
SEARCH_ENABLED=true # set to false to disable the /search route + module entirely
SEARCH_PROVIDER=auto # auto | builtin-fts | none (plugin ids selectable once Spec 2 lands)
SEARCH_LIMIT_MAX=100 # hard cap on the `limit` query param
# =============================================================================
# PLUGINS
# =============================================================================
PLUGINS_ENABLED=true # Enable plugin system
PLUGINS_DIR=./data/plugins # Plugin directory
# =============================================================================
# SECURITY
# =============================================================================
# Master API key (leave empty to disable, or set to secure value)
API_MASTER_KEY=
# First-boot default admin key. By default a cryptographically random key is
# generated (printed in the startup banner / written to data/.api-key). Set this
# to true ONLY for local development to seed the well-known, insecure
# `dev-admin-key` instead. Ignored when API_MASTER_KEY is set.
# ALLOW_DEV_API_KEY=true
# Optional server-side pepper for API-key hashing (HMAC-SHA256 instead of plain SHA-256).
# Recommended in production. Note: setting or changing it invalidates all existing key hashes,
# so re-issue keys after enabling. Leave unset to keep the current (unpeppered) behaviour.
# API_KEY_PEPPER=
# =============================================================================
# DEVELOPER SETTINGS
# =============================================================================
ENABLE_SWAGGER=true # Enable API documentation at /api/docs (set false to disable on exposed deployments)
BODY_SIZE_LIMIT=25mb # Max request body size (base64 media sends ride in the JSON body)
# CSP_UPGRADE_INSECURE_REQUESTS=false # Set false for an HTTP-only deployment on a trusted private network
# (default: on in production). Stops the browser upgrading the dashboard to https.