§01 context
A CRM for gyms and studios wanted their tenants to message leads on WhatsApp without leaving the product. Every tenant was exporting lists into a separate tool and pasting numbers by hand.
We built two services. A public one that only ingests Meta's webhooks, and an internal one that holds every credential and is reachable only from the CRM's backend. Onboarding runs through Meta's Embedded Signup, and each tenant's system user token is encrypted at rest.
§02 shape
How it’s put together
CRM backend (owns auth + tenants)
│ internal network only
▼
WhatsApp API Service ── holds App Secret + per-tenant tokens
│
▼
Meta Cloud API ──── webhooks ────▶ Webhook Service (public)
│ validate + enqueue
▼
RabbitMQ ─▶ Postgres (status, analytics)§03 hard parts
The hard parts
- 01
Not owning authorization
The CRM already knows who its users and tenants are. So the service trusts its caller and the network decides who that caller can be. Smaller surface, and any product with its own auth can plug in.
- 02
Credential isolation
The service that can send messages is never on the public internet. The one that is public can only receive. If one route leaks, it doesn't leak every tenant's WhatsApp account with it.
- 03
Onboarding state gates sending
Embedded Signup hands back a short-lived code, the backend exchanges it for a system user token, registers the number, and only then flips the project to active. Nothing sends from a project that isn't.
stack
- TypeScript
- NestJS
- PostgreSQL
- RabbitMQ
- WhatsApp Cloud API
next →
Compass
A multi-tenant CRM for real-estate sales teams, from tenant provisioning to lead assignment.