Skip to content
Osama Jenana
All work
Web SystemsAI & AutomationInfra & IntegrationsShipped

WhatsApp-Native Commerce Platform

A multi-branch store where the entire storefront is a WhatsApp conversation

Role
Full-stack — both services, dashboards, flow engine and deployment
Period
2025 — Present
Stack
Laravel 13 · Vue 3 + Inertia · Laravel Reverb (WebSockets) · Meta WhatsApp Cloud API · Fastify · Redis · MySQL · Thawani · Pest

Key figures

PHP files in app/
176
Eloquent models
23
Migrations
44
Pest test files
53
Payment and flow-engine paths are the most heavily covered
Vue components
74
Gateway source files
14
Deliberately tiny: protocol translation only, zero business logic

The problem

A multi-branch butcher and grocery business was taking every order by hand on WhatsApp. Staff typed prices from memory, orders lived in chat scrollback, and nobody could answer "what did branch three sell yesterday" without reading conversations.

The obvious fix — build an app or a web store — was the wrong fix. Their customers were already ordering successfully; they just were not being recorded. Asking those customers to install anything would have traded a working channel for a funnel with a drop-off at every step.

So the requirement inverted: keep WhatsApp as the entire storefront, and put a real system behind it. The customer experience should feel like the same conversation they were already having. The business should get catalog control, order state, branch-level reporting and payment collection.

Hard constraints

  • Customers install nothing and visit no website. WhatsApp is not a notification channel here — it is the entire product surface.
  • Meta's 24-hour customer service window is a hard platform rule: outside it, only pre-approved templates may be sent. Violating it risks the business number.
  • Each branch has its own catalog, stock and working hours, but head office needs one consolidated view.
  • Conversations are stateful and long-lived. A customer can abandon mid-order and return hours later expecting to continue.

Architecture

Two services, deliberately unequal. A Node/Fastify gateway sits at the edge and does nothing but speak Meta: verify the webhook signature, decrypt WhatsApp Flow payloads, normalise the message shape, and forward inward. It holds no database and no business rules — 14 source files in total. A Laravel application holds everything that is actually the business.

The split exists because Meta protocol churn and business logic change for entirely unrelated reasons. When Meta alters a webhook envelope or rotates Flow encryption requirements, only the gateway moves. When pricing rules or branch behaviour change, the gateway is untouched. Both directions of that boundary are authenticated with HMAC signatures, so neither service will act on a request the other did not sign.

Inside Laravel, the conversation is a state machine. A flow engine advances a customer through discrete steps, each one a class, with the current position persisted — which is what makes resuming an order hours later a lookup rather than a reconstruction. A dedicated window service tracks the 24-hour rule per conversation and decides whether a free-form message is permitted or a template dispatch is required, so platform compliance is a property of the system rather than a thing staff must remember.

Two services with an HMAC-signed boundary. The gateway speaks Meta and nothing else; Laravel holds the business. Meta protocol changes stop at the gateway, and pricing changes never reach it.HMACWhatsAppthe whole storefrontFastify gatewayverify · decryptFlow enginestate machineMySQLRedis + Queuecampaigns · syncThawanipaymentsReverb (WS)live order boardVue dashboardsHQ + per branch
Two services with an HMAC-signed boundary. The gateway speaks Meta and nothing else; Laravel holds the business. Meta protocol changes stop at the gateway, and pricing changes never reach it.
  • Fastify gateway (Node)Meta signature verification, Flow RSA/AES decryption, protocol translation — zero business logic
  • HMAC service boundaryBoth directions signed; neither service trusts an unsigned request
  • Flow engine (state machine)Step classes with persisted position, so an abandoned order resumes exactly where it stopped
  • 24-hour window serviceDecides free-form vs. approved template per conversation, enforcing Meta policy in code
  • Laravel Reverb (WebSockets)Live order boards for head office and each branch without polling
  • Thawani paymentsLocal payment rail with server-side verification
  • Vue 3 + Inertia dashboardsTwo role-separated operator surfaces over one Laravel backend

Design decisions

A separate gateway service instead of a Laravel webhook controller

ChoseStandalone Node/Fastify gatewayoverHandling Meta webhooks directly inside Laravel

A Laravel route would have been less infrastructure and fewer moving parts, and for a single-tenant store it might have been right.

Three things pushed the other way. Meta requires raw-body signature verification, which fights framework middleware that has already parsed the request. WhatsApp Flow needs RSA and AES key handling that has no business sitting next to invoice logic. And webhook delivery must be acknowledged fast — a thin service that verifies, decrypts and forwards can return immediately regardless of how slow downstream work is.

The gateway ended up at 14 files with its own crypto tests. That is the entire cost of never having to touch business code when Meta changes something.

Persisted step classes instead of parsing conversation history

ChoseExplicit state machine with a stored positionoverInferring intent from the message log each time

Reading back the conversation to work out where a customer is sounds flexible and is a trap. It makes every reply an ambiguous parse, and it gets worse as the catalog grows.

Modelling the flow as discrete step classes with the position written to the database made behaviour testable in isolation — which is a large part of why the test suite reached 53 files. It also made the abandoned-order case free: resuming is reading a row, not replaying a chat.

Encoding the 24-hour rule in a service, not in staff training

ChoseA window service that gates every outbound messageoverDocumenting the rule and trusting operators

The penalty for breaking Meta policy is not a validation error — it is quality-rating damage to the business phone number, and eventually losing it. That is an existential risk for a store whose only channel is WhatsApp.

Putting the rule in a service that every outbound message passes through means the system physically cannot send a free-form message outside the window; it dispatches an approved template instead. Compliance became a code path with a test, rather than a line in an onboarding document.

Outcome

The platform runs as two independently deployable services: 176 PHP files across 23 models and 44 migrations in Laravel, 74 Vue components for the two dashboards, and a 14-file gateway. The Laravel side carries 53 Pest test files, weighted towards the payment and flow-engine paths where a defect costs real money.

Beyond ordering, the system ships bulk campaign dispatch, Meta catalog and template synchronisation, invoice PDF generation, per-branch working hours, and an analytics package with revenue, best-selling and branch-comparison reporting. The branch mobile app is served by a documented API with its own written endpoint reference.

The result the business actually asked for: the customer conversation did not change, and everything about it is now recorded, priced and reportable.

What I'd carry forward

  • 01The best interface is sometimes the one the customer is already using. Building a "proper" storefront would have been more work and fewer orders.
  • 02When you integrate with a platform you do not control, isolate it behind a service you do. The seam pays for itself the first time the vendor changes something.
  • 03Platform policy belongs in code. Any rule whose violation can end the business should be impossible to violate by hand.
Next case studySilaPeer-to-peer messaging with no internet, no servers and no SIM