kstmostofa/laravel-whatsapp
Dual-backend WhatsApp for Laravel: a pure-PHP Meta Cloud API client plus a whatsapp-web.js sidecar for personal-number access, unified under one WhatsApp:: facade, with a Livewire admin UI, Eloquent models, webhooks, and health checks.
Architecture fit
The package is a conventional Laravel integration — service provider, config publish, migrations, WhatsApp:: facade, queued job, events, Artisan commands — plus two optional surfaces: a Livewire/Flux admin UI and a bundled Node sidecar. That shape fits a Laravel monolith or modular monolith cleanly. It does not fit a polyglot/service-oriented stack, a serverless/container-per-request platform, or anything where you cannot run a persistent Node process under Supervisor.
The strategic issue is that this is really two products under one API:
| Cloud API backend | Web sidecar backend | |
|---|---|---|
| Nature | Official Meta Graph API client | Browser automation of WhatsApp Web |
| ToS posture | Supported, compliant | Gray area, explicit ban risk |
| Runtime | None beyond PHP | Node + Chromium (~600 MB), persistent process |
| Scalability | Horizontal, millions | Session-bound |
| Unique value | Templates, verified sender | Personal number, groups, status, free-form sends |
The facade's WhatsApp::send() auto-routing by recipient shape (+9665… vs 9665…@c.us) is convenient but is also an implicit backend decision made at the call site. As a TPM I would treat that as a leaky abstraction to be constrained in code review, not a feature to lean on.
Integration feasibility
whatsapp:web:listen) per session under Supervisor/systemd, and QR pairing as a human-in-the-loop step.Technical risk
whatsapp-web.js broke — messages arriving with id: null and /chats returning {"error":"r"}. That is a structural property of browser automation, not a one-off. Expect recurring, unannounced breakage with a fix latency bounded by a single volunteer maintainer.@c.us/@g.us). Every message-handling code path has a backend branch.WHATSAPP_APP_SECRET is unset (good); sidecar binds to 127.0.0.1 with a bearer token (good); but /whatsapp/* ships without auth middleware and only logs a warning in production. That is ours to configure, not theirs.Key questions for the vendor/author (or our own discovery)
whatsapp-web.js breaks — us or the maintainer, and with what SLA?WHATSAPP_DB_CONNECTION isolation required by our data-residency/retention policy for message content?Stack fit
Laravel 11/12/13, PHP 8.2 (8.4 required on Laravel 13 due to Symfony 8), Livewire 3/4 and Flux only if we want the UI. Broadcasting is opt-in (WHATSAPP_BROADCAST=true) and pulls in Reverb/Pusher/Ably — treat as a phase-3 nicety, not a prerequisite. Everything is MIT-licensed, so forking is legally unencumbered.
Recommended posture: Cloud-first, sidecar-optional, headless-first.
Migration path Greenfield — no legacy data to move. The only migration decisions are:
WaSession/WaMessage/WaContact persistence at all, or keep our own domain tables and consume the events. I lean toward consuming events into our own schema so the package stays a transport, not a system of record.WHATSAPP_DB_CONNECTION to isolate message content on a separate connection with its own retention policy.composer update (no migrations, no config, no API changes), which is a good sign for patch-level upgrade friction.Compatibility
composer.json and pin PHP/Laravel in CI against the supported matrix.WHATSAPP_UI_CSS_MODE=standalone if we adopt the UI — it ships precompiled CSS served only on /whatsapp/*, so it cannot collide with our Tailwind/Bootstrap/Vite pipeline. This is the single best integration decision available here.livewire/flux entirely if we go headless; the facade, events, jobs, webhook receiver, and commands all work without it.Sequencing
| Phase | Scope | Gate to proceed |
|---|---|---|
| 0 | Spike: Cloud API sandbox send + template + inbound webhook → our event listener | Meta business verification complete; permanent token issued |
| 1 | Production Cloud API: templates for transactional messaging, SendMessage job on queue, HMAC webhook behind our middleware, whatsapp:health --json --exit-code wired into monitoring |
p95 send latency and template approval rate acceptable |
| 2 | Optional UI at /whatsapp behind our auth middleware, standalone CSS, separate DB connection |
Support/ops team confirms it replaces an existing tool |
| 3 | Sidecar pilot, isolated: dedicated SIM, 127.0.0.1 binding, strong WHATSAPP_WEB_TOKEN, Supervisor per session, alerts on session drop |
Business owner signs off on ban risk; fallback comms channel live |
| 4 | Scale/broadcasting: bulk send (currently unbuilt), Reverb | Only if volume justifies it |
Note that "bulk send job with rate limiting" is an unchecked roadmap item. If broadcast/marketing messaging is in scope, we are building that ourselves — do not plan around it.
Abstraction guidance
Wrap the facade behind our own NotificationChannel interface with sendTemplate() and sendText() as the only methods we call. Do not let WhatsApp::send()'s recipient-shape routing appear in application code. This gives us a clean exit to Twilio, 360dialog, or a BSP if the package stalls.
Maintenance
whatsapp-web.js's compatibility — plus a Chromium binary we must keep patched for CVEs independent of the package's release cadence.Support
Scaling
whatsapp:web:listen process per session, session state on local disk. It is a single-instance component and dictates that our deployment target has persistent local storage and cannot be a stateless autoscaling group.Failure modes to instrument before launch
| Failure | Symptom | Mitigation |
|---|---|---|
| WhatsApp Web minification change | id: null messages, /chats returning {"error":"r"} |
Alert on sidecar 5xx rate and null-ID insert rate; keep pinned whatsapp-web.js version |
| QR session expiry / logout | Sidecar silently stops receiving | whatsapp:health --json --exit-code in cron + page ops; treat as PagerDuty-class |
| Chromium crash / OOM | Process dies, PID file stale | Supervisor restart + whatsapp:sidecar:status check |
Missing WHATSAPP_APP_SECRET |
Webhook returns 503 fail-closed — inbound silently dead | Config lint at boot in CI; assert in healthcheck |
| Missing UI auth middleware | /whatsapp publicly readable, contains customer conversations |
Set ui.middleware in config; add a deploy-time assertion |
| Duplicate rows on no-ID messages | Inflated conversation counts | v1.0.3 inserts instead of upserts; reconcile by (from, body, timestamp) |
| Meta 24-hour window / template rejection | Sends fail with permanent codes, no retry | Route marketing copy through approved templates only; surface rejections to business owner |
| Sidecar account ban | Number disabled, history lost | Dedicated non-primary number; parallel email/SMS fallback; feature-flag kill switch |
| Single-maintainer abandonment | No fixes for upstream breakage | Fork rights under MIT; keep the package behind our own interface |
Ramp-up
TPM recommendation Adopt the Cloud API backend in production now — it is low-risk, well-scoped, and removes a real integration cost. Treat the web sidecar as a separate, gated pilot with a dedicated number and an explicit, written acceptance of ToS/ban risk; do not merge it into the critical path. Ship headless first and only enable the Livewire UI if a named team will actually use it. Keep the package behind an internal notification interface so that a stalled maintainer, a WhatsApp Web change, or a Meta policy shift is a swap rather than a rewrite.
How can I help you explore Laravel packages today?