Releases: rmyndharis/OpenWA
v0.7.13
Fixed
- Bulk batch ids are unique per session, not globally. A batch id claimed by one session no longer prevents another session from using the same id — the uniqueness constraint is now scoped to
(session, batchId), matching the per-session lookup, so an explicit cross-session reuse no longer fails with a500. Reusing an id within the same session is still rejected with a clear400. Existing databases are migrated in place. (#531) - A message arriving while a session is being deleted is no longer persisted as an orphan. The inbound-message handler re-checks that the session is still live after its asynchronous processing, so a message that races a session deletion can't leave behind a
messagesrow (which has no cascade) for a session that no longer exists. (#531) - Per-session stats return a consistent
lastActivetimestamp on SQLite and PostgreSQL.GET /stats/sessions/:idpreviously emitted a differenttopChats[].lastActiveformat depending on the database (an ISO date-time on PostgreSQL versus the stored text on SQLite); it is now formatted to a stableYYYY-MM-DD HH:MM:SSon both. (#533) - The uuid id default now works on PostgreSQL 12 and older. Id generation relies on a
gen_random_uuid()column default, which is a core built-in only from PostgreSQL 13; on older servers it lives in thepgcryptoextension. The migration now enablespgcryptofirst, so a fresh deploy against PostgreSQL ≤ 12 no longer fails on startup or first insert. (#533) - The audit-log listing no longer loads the whole table for a large
limit.GET /auditclamps its page size to a maximum of 200, so an oversizedlimitcan't pull the entireaudit_logstable into a single response. (#536) - Migration reverts are idempotent on a synchronize-bootstrapped database. The
baileys_stored_messagesandwebhook_delivery_failuresmigrations now drop their indexes withIF EXISTS, so adown()no longer errors when the named indexes were never created. (#536) - Bulk send always releases its in-flight marker. A batch whose session engine was missing, or that threw mid-processing, previously left a stale entry in an in-memory tracking map; the marker is now released on every exit path. (#536)
Security
- Hook re-entrancy is now blocked for sandboxed plugins too. A plugin running in the worker-thread sandbox could re-fire the hook it was handling by issuing a capability call (for example, sending a message from within a
message:sendinghandler), because the re-entrancy guard did not span the worker boundary — looping the event back into the plugin without bound. The host now runs each worker-initiated capability call inside the in-flight hook context, so such a re-fire is short-circuited exactly as it already was for in-process plugins. (#532) - Docker container teardown is constrained to OpenWA-managed services. The
POST /infra/restartendpoint passed itsprofilesToRemovelist straight to container removal, which resolved containers by a name substring — so an unrecognized or empty profile could stop and remove an unrelated container. Teardown is now restricted to the managed allowlist (postgres,redis,minio) and container resolution requires an exactopenwa-<service>name match. (#534) - Failed API-key authentication attempts are now recorded in the audit log. Rejected or denied keys (invalid, disabled/expired, IP- or session-scope-denied, or insufficient role) previously left no audit entry; the gateway now logs an
api_key_auth_failedevent with the client IP, method, path, and reason, giving administrators a forensic trail for credential probing. Audit logging stays best-effort and never affects the request outcome. (#535) - The SSRF guard blocks the deprecated IPv6 site-local range (
fec0::/10). Webhook and server-side media URLs are now rejected when they resolve intofec0::/10, closing a gap alongside the already-blocked unique-local and link-local ranges. (#536) - Session-scoped MCP tools require a session id before authorization. A session-scoped tool invoked without a session id is now rejected, so a session-restricted API key can't be used to drive such a tool against a session outside its scope. (#536)
- Contact-card vCards are sanitized on both engines. Sending a contact whose name or number contained CR/LF could inject extra vCard fields on the whatsapp-web.js engine; both adapters now build the vCard through one shared sanitizing helper (CR/LF stripped, digits-only
waid). (#537)
v0.7.12
Added
- Brazilian Portuguese (pt-BR) locale. The dashboard is now available in Português (Brasil) — all 9 navigation sections, toasts, dialogs, and form labels are translated. Select it from the language picker on the login screen or the sidebar. Thanks @A831ARD0.
Fixed
- The engine fallback no longer silently starts the wrong engine. If the configured engine (
ENGINE_TYPE, e.g.baileys) is unavailable and the legacy direct-creation fallback is reached, it now fails with a clear error instead of silently constructing the whatsapp-web.js adapter. (#527)
Security
- Application logs redact secret-valued metadata. The values of secret-named log fields (
password,secret,token,api-key,authorization,credential,pepper,private-key) are replaced with[REDACTED]before a line is written — defense-in-depth so a stray log statement can't leak a credential. (#527)
Performance
- Failed media sends and completed bulk batches no longer retain their base64 payload. A failed media send kept its (often multi-MB) base64 in the message row, and a completed bulk batch kept every message's base64 in
message_batchesindefinitely — both are now stripped (mimetype/filename kept), so themessagesandmessage_batchestables don't grow without bound. (#524) - The dashboard chat view no longer caches full media base64. Chat history is fetched without media and the per-chat cache is evicted sooner, so browsing several media-rich chats no longer risks OOMing the tab; older history media shows a
📎 Mediaplaceholder and recent media still renders. (#525)
v0.7.11
Added
- Disappearing-messages support (Baileys engine). Outbound messages now honor a chat's disappearing-messages timer and set it on each send (text, media, and replies), so recipients no longer see "This message won't disappear — the sender may be using an older version of WhatsApp." The timer is learned from inbound messages — the reliable source, since the cached chat setting is often absent for a long-standing timer — and resolved across both phone and
@lidchat identifiers so it applies on LID-migrated 1:1 chats, with a fallback to the chat's cached setting. It is applied only when a positive value is known; when it's unknown or disabled, the per-message expiration is omitted, exactly as before. Reactions, deletes/revokes, and status posts are unaffected. Thanks @ulises2k. (#473, #513) - Selective skip for disappearing messages. New
STORE_EPHEMERAL_MESSAGESenv var (defaulttrue). Set tofalseto skip persisting and dispatching incoming disappearing messages (those withephemeralDuration > 0) — no DB insert, no webhook dispatch, no websocket event. Backward compatible; existing deployments are unaffected. TheephemeralDurationfield is also surfaced onIncomingMessagefor consumers that want to handle it themselves. Thanks @spidgrou. (#506) - Durable dead-letter record for failed webhook deliveries. A webhook delivery that permanently fails — exhausting its retries or being rejected before it is sent — is now persisted to a new
webhook_delivery_failurestable instead of disappearing when its job is evicted from the queue. Operators can review the recorded failures (endpoint, event, status, error, attempts) through a new admin endpoint,GET /webhooks/delivery-failures. (#520)
Fixed
- Deleting a session now removes its message history and bulk batches. The
messagesandmessage_batchestables had no cascade fromsessions, so a deleted session left its rows behind — growing the largest tables without bound and skewing dashboard statistics. They are now removed in the same transaction as the session. (#504) - Deleting a session while it is reconnecting no longer leaks its engine. A delete that landed during the multi-second engine initialization of an in-flight reconnect (or start) could leave the freshly-launched browser/socket registered under the now-deleted session, still counting toward the concurrent-session limit. The post-init guard now re-checks that the session still exists before keeping the engine. (#521)
- Inbound media downloads are bounded by a wall-clock timeout. A slow or stalled inbound media transfer could hold a download slot — and, on the Baileys engine, the entire inbound-message pipeline — open indefinitely. Downloads now time out (
MEDIA_DOWNLOAD_TIMEOUT_MS, default 30s) and the message is delivered with the media omitted. (#510) - Webhook delivery identifiers stay consistent with the signed body. The
X-OpenWA-Idempotency-Key/X-OpenWA-Delivery-Idheaders could diverge from the signed payload when awebhook:beforeplugin returned a modified payload, and all webhooks for an event shared onedataobject. Each webhook now receives an isolated copy of the data and the server-generated identifiers are authoritative. (#512) POST /auth/validateno longer double-counts key usage and now validates IP-restricted keys correctly (it previously reported a valid IP-pinned key as invalid). (#507)⚠️ GET /settingsnow requires an ADMIN key (behavior change) — matching the rest of the configuration surface; it was previously readable by any authenticated key. A client that read settings with a non-admin key must switch to an ADMIN key. (#514)- Bulk-message
batchIduniqueness is scoped per session, so two sessions can reuse a batch id and neither can probe the other's id namespace. (#515) ⚠️ Boot-time configuration validation now rejects0for the rate-limit limits and the webhook timeout (behavior change) — values that silently disabled throttling or aborted every delivery. A deployment that set0to disable these must remove the override or use a positive value. (#516)- SSRF protection now blocks the RFC6052 IPv4-translatable IPv6 form (
::ffff:0:a.b.c.d), closing a gap where an internal address could be reached behind a NAT64/SIIT translator. (#518) - Per-key IP allowlist now uses the shared, hardened IP matcher and rejects a malformed client address instead of coercing it into an allowed range. (#519)
- Dashboard: the Infrastructure page is no longer rendered for non-admin roles, and image-attachment preview object URLs are released after use. (#508)
- Released a small in-memory leak: a deleted session's stored failure reason is now cleared. (#505)
- The webhook worker now connects to the configured Redis. Configuration from
.envand the dashboard-saved file is loaded before the application modules are evaluated, so the webhook delivery worker reads its Redis host/port/password from the configured values instead of falling back to a local default when those are supplied by file rather than the process environment. (#523)
Performance
- Configurable webhook worker concurrency (
WEBHOOK_WORKER_CONCURRENCY, default 10): a single slow or unresponsive receiver no longer head-of-line-blocks delivery for every other webhook. (#511) - Dropped a redundant single-column index on
messages(sessionId)already covered by the existing composite indexes, reducing write-time overhead on a high-volume table. (#509)
v0.7.10
Added
-
WhatsApp Status posting (Baileys only). The three status
send-*endpoints now post to the status feed on the Baileys engine:POST /api/sessions/:id/status/send-text,/send-image, and/send-videoaccept a requiredrecipients[]body field (1–256 JIDs, each@c.usor@lid; passed to the engine asstatusJidList— an empty array is rejected with400). Image/video take an optionalimage.mimetype/video.mimetype; the service defaults toimage/jpeg/video/mp4. A whatsapp-web.js session returns501: WA Web removedWAWebStatusGatingUtils.canCheckStatusRankingPosterGatingaround 2026-04-30, so the wwebjs path is upstream-blocked.@c.usrecipients are reliable;@lidis best-effort (unverified), and the posting account's own phone may briefly show a "waiting for this status update" notice while recipients view it normally. Thanks @CharlesLightjarvis for the report. (#455) -
Visible placeholder for skipped inbound media. When
MEDIA_DOWNLOAD_ENABLED=false(or a media item is over the byte cap), an incoming media message now carries anomittedmarker and the dashboard chat renders a📎 Mediaplaceholder instead of a bare timestamp. The marker reuses the existing{ mimetype, omitted, sizeBytes }shape on both the whatsapp-web.js and Baileys engines, so webhook/n8n/dashboard consumers see one consistent contract for "media was present but not downloaded." Thanks @spidgrou. (#501)
Fixed
-
Status image/video no longer hardcode
image/jpeg/video/mp4. TheSendImageStatusDto/SendVideoStatusDtomedia input now accepts an optionalmimetype; the service appliesmimetype ?? 'image/jpeg'(or'video/mp4') instead of always passing the hardcoded value to the engine. (#455) -
Clean install on Node 22+ / npm 11.
@nestjs/websocketsis now declared as a direct dependency — it was only resolving transitively via@nestjs/platform-socket.io, so stricter installs failed withTS2307: Cannot find module '@nestjs/websockets'. Thepostinstallscript also no longer triggers Node'sDEP0190deprecation:shell: trueis retained (so Windows still resolvesnpmvianpm.cmd) but the command is now passed as a single string instead of an args array. Thanks @abdullah4tech. (#500)
Changed
- Italian translation update. Improved the
messageTesterpage title in the Italian (it) dashboard locale to use natural Italian instead of an anglicism. Thanks @albanobattistella. (#497)
v0.7.9
Added
- Bounded list pagination.
GET /sessionsandGET /webhooks(and the matching agent tools) now acceptlimit(1–1000, default 1000) andoffsetquery parameters, so large deployments can page through results instead of receiving an unbounded list. (#496) - Concurrent-session cap. New
MAX_CONCURRENT_SESSIONSenv (default0= unlimited) caps how many WhatsApp engines may run or initialize at once, protecting memory/Chromium-constrained hosts. (#496) - Configurable Redis connect timeout. New
REDIS_CONNECT_TIMEOUT_MS(default5000) bounds how long the queue and cache connections wait when reaching Redis. (#496)
Fixed
- Webhook delivery during a Redis outage. The webhook queue producer now fails fast instead of buffering indefinitely when Redis is unreachable, falling back to direct (signed, idempotent) delivery; the queue Worker keeps its offline queue so it still tolerates brief reconnects. (#496)
- Accurate session stats at scale.
GET /sessions/statsaggregates status counts in the database, so totals stay correct on deployments with more sessions than the list cap. (#496) - Plugin storage key safety & portability. Plugin storage keys are validated and encoded to filesystem-safe filenames (JID-style keys now work on Windows), with backward-compatible reads/deletes of pre-existing files. (#496)
Changed
- Refreshed project documentation, roadmap, and testing strategy against the current baseline. (#496)
v0.7.8
Added
- Optional inbound-media skip. New
MEDIA_DOWNLOAD_ENABLEDflag (defaulttrue) lets operators skip downloading inbound media entirely on both the whatsapp-web.js and Baileys engines — useful for text-only or low-resource deployments. When disabled, inbound messages omit themediafield and reporthasMedia: falsein webhooks and the dashboard. Thanks @spidgrou. (#492)
Fixed
- External-S3 setups no longer silently fall back to local disk after upgrading:
docker-compose.ymlagain forwards the legacyS3_ACCESS_KEY/S3_SECRET_KEY(alongside the canonicalS3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY) so an existing.envkeeps reaching the container, and the legacy names are blank-cleared so they can't shadow the dashboard config. (#488 follow-up) - The production default-secret guard no longer skips a weak credential for a host-pinned external datastore just because the built-in flag is set: the built-in exemption now requires both the
*_BUILTINflag and an internal host (postgres/minio), so an external Postgres/MinIO with a default password is still rejected in production. (#488 follow-up) - The Infrastructure page now shows an error + retry (instead of an editable form seeded from defaults) when the live
/infra/statuscan't be loaded, so a save can no longer flip a running built-in database/Redis/storage to external+empty. (#488 follow-up) /infra/statusno longer blocks on the WhatsApp Web version registry fetch, and that fetch is rate-limited after a failure, so a firewalled/offline host no longer stalls up to 5s on every status poll and every session start/reconnect. (#488 follow-up)- A replayed
message.sentWebSocket echo no longer downgrades a chat message already shown as delivered/read; the live-append path now applies the same forward-only delivery-status merge as the ack path. (#484 follow-up)
Changed
- Italian translation update. Refreshed the Italian (
it) dashboard locale. Thanks @albanobattistella. (#491)
v0.7.7
Added
- Dashboard chat thread UX: URLs in messages are now clickable links, WhatsApp text formatting (bold/italic/strikethrough/monospace) renders, images open in a photo lightbox, and the scroll position is remembered per chat. Thanks @softronicve. (#484)
- The Infrastructure page now shows the actual WhatsApp Web build the whatsapp-web.js engine is using (e.g.
2.3000.1042251103-alpha) and how it was chosen (pinned viaWWEBJS_WEB_VERSION, auto-resolved, or native), surfaced via/infra/status. The engine card previously showed only the npm library version (whatsapp-web.js 1.34.7), which is unrelated to the WA Web build that actually governs connection stability. (#488) - Infrastructure data backup & restore: export all Data-DB tables to a JSON file and import them back, wired into the database-switch flow. When you change the database backend, the restart dialog now warns that the new database starts empty and offers a one-click backup before switching; a storage switch warns that existing media is not moved. (#488)
- The Infrastructure page flags any database/redis/storage setting that is pinned by an environment variable (its running value differs from the saved config), so it's clear a dashboard change won't apply until that variable is unset, instead of the control silently having no effect. (#488)
- The storage card now warns when S3 is selected but unreachable (a dead/misconfigured bucket no longer shows a misleading green badge), via a new
s3Availablefield on/infra/status; the check re-probes (throttled) rather than latching the boot-time result, so a bundled MinIO that comes up after the app self-corrects. A backup import that exceeds the request size limit now reports an actionable message (raiseBODY_SIZE_LIMIT) instead of a bare "Payload Too Large". (#488) - Data-loss & availability hardening for the new infra flows: importing a backup now refuses an empty/garbage file (it no longer wipes the database and reports success) and asks for confirmation first; selecting the built-in Postgres/MinIO no longer crash-loops a production boot on the default-secret guard (the bundled containers run on the internal-only network); and a transient failure fetching the WhatsApp Web version is no longer cached, so it retries instead of permanently falling back. (#488)
- Human-readable console logs: the
LoggerServicenow renders a colorized, NestJS-style line ([OpenWA] <pid> - <timestamp> <LEVEL> [Context] <message>with dimmedkey=valuemetadata and stack traces on their own line) instead of always emitting raw JSON, so application logs line up visually with NestJS's own framework logs. The format defaults to structured JSON in production (NODE_ENV=production, for containers and log aggregators) and human-readable pretty everywhere else, and can be pinned withLOG_FORMAT=pretty|json.NO_COLOR/FORCE_COLORare honored. JSON output is byte-for-byte unchanged when selected. (#469)
Fixed
- whatsapp-web.js sessions that scanned the QR then immediately disconnected (looping
qr → authenticating → disconnected) when noWWEBJS_WEB_VERSIONwas pinned — the common Docker default. The engine now auto-resolves the current known-good WhatsApp Web build from the wppconnectwa-versionregistry and pins it, instead of relying on whatsapp-web.js's auto-select which could latch onto an incompatible bleeding-edge build that authenticates but never reaches "ready".WWEBJS_WEB_VERSION=offkeeps the old native auto-select; an explicit version still pins exactly. (#488) - Dashboard message-analytics charts no longer silently vanish on PostgreSQL:
/stats/messages(top-chats) ordered by an unquoted mixed-case alias (ORDER BY messageCount), which PostgreSQL case-folds and rejects withcolumn "messagecount" does not exist(500). It now orders by the aggregate directly, so the query — and the dashboard charts it feeds — work on PostgreSQL as they already did on SQLite. The chart section also shows a clear notice on a real error instead of rendering nothing (it previously treated every error as a non-admin 403 and hid itself). (#488) - The Infrastructure page now shows what is actually running for the database, Redis, storage, and engine — the badge/selected card follow the live
/infra/statusinstead of the saveddata/.env.generated, which could disagree when a setting is supplied via environment variable. Previously a stack running PostgreSQL viaDATABASE_TYPE=postgresshowed "SQLite" (the first-run default still in the saved file)./infra/statusnow also reportsredis.enabled. (#488) - The "Use Built-in PostgreSQL/Redis/MinIO Container" toggles now reflect whether OpenWA's bundled container is actually running and backing the service (detected from the labeled container + the configured host), not just the saved intent — so a Postgres stack started via the
postgrescompose profile correctly shows built-in, and a stopped/external one shows off. Falls back to the saved flag when Docker isn't reachable. (#488) - Switching away from a built-in backend (built-in → external/disabled) now tears down the bundled container reliably even after a page reload: removal is derived server-side from the saved
*_BUILTINflags + the running labeled containers, instead of only trusting the browser's in-memory list (which reset on reload and left the container orphaned). Named volumes are preserved, so re-enabling reuses the data. (#488) - Dashboard "by type" message chart: each message type now gets a stable, distinct color keyed by type name (with a deterministic hash fallback) instead of a rotating array-index palette, so a slice keeps its color when the set of present types changes between requests and types past the eighth no longer collide. (#486)
- Removed the oversized decorative watermark icons bleeding through the dashboard stat cards. (#488)
- Dashboard switches for the database, Redis, and storage backends now actually take effect after a restart, matching how the engine switch already worked. The bundled
docker-compose.ymlforwards these settings blank (${VAR:-}) so the dashboard's saved selection (indata/.env.generated) is honored, while a real value set in your.env/host still pins it (and the UI now says so). Previously compose forwarded concrete defaults that silently shadowed the dashboard's choice, so switching had no effect under Docker. (#488)
Changed
⚠️ docker-compose.ymlnow forwards the S3 credentials under their canonical namesS3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY(and addsS3_REGION), matching what the app and dashboard read. The legacyS3_ACCESS_KEY/S3_SECRET_KEYare still accepted as a fallback, so existing setups keep working, but updating your.envto the canonical names is recommended. (#488)⚠️ Database/Redis/storage selection is now sourced from the dashboard-manageddata/.env.generatedwhen not pinned by an environment variable (see Fixed, above). If you previously relied on the compose file's concrete defaults overriding a staledata/.env.generated, set the value explicitly in your.env/host to pin it. First-run defaults (SQLite, local storage, Redis off) are unchanged. (#488)
v0.7.6
Changed
- CI now runs the dashboard unit tests, and re-runs the client-SDK suites when a server DTO or the engine interface changes (not only on SDK edits), so contract drift is caught at its source. (#478)
- The Postgres connection pool now applies query/connection timeouts (
statement_timeout,idleTimeoutMillis,connectionTimeoutMillis) on the runtime connection, so a stuck query or a saturated pool fails fast instead of hanging requests. The migration connection keeps idle/connection timeouts but neverstatement_timeout, so a longCREATE INDEXis not aborted. Env-tunable (DATABASE_STATEMENT_TIMEOUT_MS,DATABASE_IDLE_TIMEOUT_MS,DATABASE_CONNECTION_TIMEOUT_MS), conservative defaults,0disables; SQLite is unaffected. (#480)
Fixed
- A plugin whose enable failed after it had already subscribed hooks no longer leaves stale hook registrations behind; a later successful enable could otherwise dispatch each event to the plugin more than once. (#477)
- The WebSocket
message.ackevent now carries the same{ id, messageId, status, ack }shape over the socket as the matching webhook does — the socket previously omittedidand the legacyack. (#477) - Reconnect timers are no longer stacked when two disconnects arrive back-to-back, and a terminal engine failure now cancels any pending reconnect so a
FAILEDsession cannot be resurrected by a stale timer. (#477) - The dashboard recovers from a stale lazy-loaded chunk after a redeploy with a single guarded reload instead of replacing the whole UI with the error screen; the Content-Security-Policy
img-srcnow allowsblob:so the outgoing image-attachment preview renders. (#477) - The Baileys engine's number-check (
GET /sessions/:id/contacts/check/:number) now returns a neutral<phone>@c.usid, matching the whatsapp-web.js engine, instead of a raw@s.whatsapp.netid. (#477) - The data export/import now includes the
lid_mappingsresolution cache, so a backup/restore or a SQLite↔PostgreSQL migration no longer drops it. (#477) - The JavaScript client SDK applies the JSON
Content-TypeandX-API-Keyafter caller-supplied headers, so they can no longer be overridden bydefaultHeaders(matching the Python and PHP SDKs); an unfollowed redirect (HTTP status0) now raises a clear error instead ofOpenWA API 0. (#478) - The infrastructure status endpoint reports the active S3 bucket when storage is in S3 mode, instead of only the unused local media path. (#478)
- The migration CLI now honors the dashboard-written
data/.env.generated, somigration:run:prodtargets the configured database (e.g. PostgreSQL) instead of silently defaulting to SQLite. (#479) - The first-run generated config writes
STORAGE_LOCAL_PATH(the key the backend reads) instead of the deadSTORAGE_PATH. (#479) - The Sessions page now keeps the shared dashboard cache in sync, so creating/stopping/deleting a session no longer leaves the Dashboard showing stale session counts or status until a refresh. (#479)
Security
- The startup banner prints the full admin API key only when it is first created; on subsequent boots the key is masked, so the live credential is not re-written to the log pipeline on every restart. (#478)
- The production secret guard now rejects a placeholder
REDIS_PASSWORD(e.g.changeme); an empty/unset password is still allowed so passwordless private-network Redis continues to boot. (#478) - The published PHP SDK package no longer ships its test suite, PHPUnit config, or
composer.lock. (#478) - The production weak-secret guard now also rejects the common defaults
123456,qwerty,root,test, anddemo. Matching stays an exact full-value comparison, so a strong secret that merely contains one of these words is not blocked. (#480) - The gateway now logs a startup warning when
API_KEY_PEPPERis unset in production (stored API-key hashes then use plain SHA-256). Advisory only — enabling a pepper invalidates existing key hashes, so it stays opt-in and is never enforced. (#480)
v0.7.5
Fixed
- The stats/analytics endpoint no longer crashes on PostgreSQL. The message time-series query grouped by an output alias named
timestamp— a reserved type keyword in PostgreSQL — soGROUP BY timestampwas not read as the alias and the query failed with "column m.createdAt must appear in the GROUP BY clause" (SQLite tolerated it, so unit tests on the SQLite test DB never caught it). The alias is nowbucket; the API response field is unchanged. (#474)
Documentation
- Added a Traefik / Coolify reverse-proxy guide to the troubleshooting FAQ: WebSocket forwarding, the
docker-proxydouble-hop that causes intermittent504s behind Coolify (held-open Socket.IO connections exhausting the pool to the single-port upstream), and idle-timeout tuning. (#467)
v0.7.4
Fixed
- WebSocket events are now delivered exactly once to a client subscribed to overlapping rooms (for example both a specific event and the
*wildcard for the same session). The real-time fan-out previously sent one copy per matching room, so such a client could receive the same event two to four times. The bundled dashboard was unaffected (its pages subscribe to disjoint rooms); this fixes duplicate delivery for custom WebSocket clients. (#468) session.authenticatedandsession.disconnectedare now emitted over the WebSocket (with{ phone, pushName }and{ reason }respectively), matching the existing webhook payloads. They were advertised as subscribable but were only ever delivered via webhooks, so socket subscribers never received them. (#468)- The infrastructure status endpoint (
GET /api/infra/status) now reports the actual media storage path — it readsstorage.localPath(default./data/media), the key the storage service uses — instead of a non-existentstorage.pathkey that always reported./uploads. (#472) - The JavaScript client SDK's
timestampfields (MessageResponse,MessageRecord) are documented as Unix seconds (the real passed-through value, previously mislabelled milliseconds), and the PHP SDK'sClient::request()is correctly typed (mixed $body): mixed). (#472)
Changed
- The WebSocket
group.join/group.leave/group.updateevents are no longer accepted as socket subscriptions — they have no engine source and were never delivered on the socket. Subscribing to one now returns a clear validation error instead of silently never delivering. They remain reserved on the webhook side. Webhook subscriptions are unaffected. (#468)
Documentation
- The
docs/set was reconciled against the v0.7.3 implementation — API specification and collection, operational runbooks, troubleshooting, system architecture, security, database, dashboard, SDK, and plugin docs — correcting drift accumulated across releases. (#471)