Added
- PostgreSQL schema selection via
POSTGRES_SCHEMA. OpenWA's tables and the TypeORM migration ledger can now be placed in a dedicated Postgres schema (defaultpublicpreserves historical behavior). SetPOSTGRES_SCHEMAto isolate OpenWA from other apps sharing a database, or to use a managed-Postgres project schema. The schema must already exist (the built-in container creates it; for external Postgres runCREATE SCHEMA <name>;once). SQLite ignores this setting. The dashboard Infrastructure page exposes the field, and the environment variable is validated as a legal, non-reserved Postgres identifier at boot.
Changed
- OpenAPI/Swagger tag hygiene. Every controller tag is now declared in the API document (ten were used but undeclared), the three Integration Fabric controllers gained
@ApiTags, and the tag casing is uniform — so/api/docsgroups every endpoint under a described tag instead of leaving some ungrouped. - Graceful shutdown now drains on
SIGTERM/SIGINT(rolling deploys,docker stop, Ctrl+C), not just on the admin restart endpoint. On a termination signal the app flips readiness to503immediately so a load balancer/orchestrator stops routing, keeps serving in-flight requests for a bounded grace window, then tears down and exits deterministically. ⚠️ Behavior change: adocker stop/ redeploy now takes up to the grace window (SHUTDOWN_DELAY_MS, default 3s in production, 0 in dev) plus teardown instead of tearing down instantly, and the process now exits0on a clean signal. In Docker setstop_grace_period≥SHUTDOWN_DELAY_MS+ your worst-case teardown (the bundled compose now sets45s); for Kubernetes setterminationGracePeriodSecondsaccordingly. A second signal during the drain forces an immediate exit. The whatsapp-web.js engine no longer lets Puppeteer install its own signal handlers (which previously killed Chromium at signal time /exit(130)before the drain could run). - The bundled Docker Compose stack pins its
docker-socket-proxyandminioimages to explicit tags (they were on:latest) for reproducible, non-drifting deploys, and a Dependabotdockerecosystem was added so base and stack images keep receiving update PRs. A Node>=22enginesfloor +.nvmrcwere declared, and the transitive install-time Scarf telemetry (viaswagger-ui-dist) is disabled.
Fixed
- The Integration Fabric now works on PostgreSQL.
conversation_mappingsandintegration_delivery_failuresdeclared@PrimaryGeneratedColumn('uuid')ids but their columns were created without a PostgresDEFAULT gen_random_uuid(), so on PostgreSQL every first insert failed with aNOT NULLviolation onid— breaking the plugin conversation-mapping upsert (e.g. the Chatwoot handover) and the ingress dead-letter write. SQLite was unaffected because its driver mints the uuid client-side, which is why it went unnoticed. A forward-only migration adds the default (no-op on SQLite). A new CI job now applies the full migration chain against a real PostgreSQL and asserts every generated-uuid primary key has a database default, so this dialect gap can't recur. - Indexed
webhooks.sessionId. The webhook dispatch path looks up a session's active webhooks bysessionIdon every emitted event, so on a busy session this was a full table scan of thewebhookstable per event (the foreign-key column carried no index). A cross-dialect index migration — plus the matching entity index — makes the lookup index-backed. - Boot now rejects a non-canonical boolean feature flag instead of silently disabling the feature.
QUEUE_ENABLED,MCP_ENABLED, andSERVE_DASHBOARDare read with an exact=== 'true'/!== 'false'comparison, so a typo (True,1,yes) or a stray trailing space/CR (a Windows-edited env file forwarded verbatim bydocker run --env-file) silently (dis)abled the feature with zero diagnostics. These are now validated at startup and boot fails fast naming the offending key. ⚠️ Behavior change: a deployment currently booting with such a value (e.g.QUEUE_ENABLED=1) will now refuse to start until corrected totrue/false/unset — includingSERVE_DASHBOARD=0/no, which was silently serving the dashboard and will now correctly disable it once set tofalse. - A fatal uncaught exception is now written to the structured log (with its stack and origin) before the process exits, instead of only a raw stack on stderr that the log pipeline missed. This is observe-only: the crash-and-restart posture is unchanged (the container restart policy still fires and the process never continues on corrupted post-exception state).
POST /infra/import-datano longer swallows a genuine database error while clearing tables. The table-clearing step tolerated only a genuinely-absent table but previously used a blanket catch, so an I/O/lock error (or an aborted transaction) could let a restore commit a merged rather than replaced dataset on SQLite. Such errors now surface and roll the whole import back (a real fault returns a500carrying the actual cause); the intended tolerance for a missing table is preserved.- A session no longer schedules a reconnect while the process is shutting down — a disconnect during the drain window would otherwise launch a fresh Chromium racing the shutdown teardown. The session is left
DISCONNECTED(a later start / auto-restore re-initializes it cleanly). - Documentation & config accuracy.
.env.examplenow documentsPORT(the port the app binds to on bare metal) distinctly from the Compose-only host-publishedAPI_PORT, and adds theQUEUE_ENABLED/CACHE_ENABLEDtoggles.SECURITY.md's supported-versions table and the Java SDK install snippets are refreshed to the current releases. The unuseduuid/@types/uuiddependency was removed, and stale "not yet wired" comments on the plugin ingress-manifest validation (which the loader has called since it shipped) were corrected. - The bundled Docker Compose stack no longer kills Chromium mid-spawn under multi-session
whatsapp-web.jsworkloads (#636). The per-containerpids_limitshipped at512since the#243hardening pass — a fork-bomb guard chosen without accounting for Chromium's multi-process model.whatsapp-web.jsruns a full Chromium instance per session (browser + renderer + GPU + zygote + utilities), and WhatsApp Web is itself process-heavy, so ~4 concurrent sessions already approached 512 and the next session's Chromium was killed mid-spawn whenfork()returnedEAGAIN— surfacing in the API as aFailed to launch the browser process: Code: nulllaunch failure with no useful log (the dbus/crashpad noise in the log is non-fatal). The default is now2048(fits ~8–10 sessions with startup-spike headroom), exposed asOPENWA_PIDS_LIMITfor larger fleets. The limit is a cgrouppids.maxceiling, not an allocation — raising it is a no-op for light containers, so this is safe for thebaileysengine (single-process, no Chromium, a handful of PIDs regardless). The fork-bomb guard stays finite (-1/unlimited is explicitly discouraged). A new troubleshooting entry distinguishes the three causes ofCode: null(PID exhaustion vs OOM-kill vs the XDG/crashpad crash already fixed earlier), since the cause isn't visible in the log withoutdocker stats/dmesg.