Audience: Engineering team, Principal Architect, client reviewers.
Last updated: 2026-05-19
Snapshot: Commit 95c85ed on origin/main.
- Technology Stack
- Multi-Tenancy Architecture
- Current State — What Already Exists
- Business Requirements
- Implementation Phases
- Day-by-Day Deliverables
- Cross-Track Dependencies
- Target State — Lead Funnel
- Client Onboarding Flow
- Project Conventions
- Quick Setup
- Source Evidence
| Layer | Technology | Version / Notes |
|---|---|---|
| Runtime | Bun | Platform runtime (>=1.3.8) |
| Framework | NestJS 11 | Fastify adapter |
| Database | MongoDB 8.x | Multi-cluster via Mongoose 9 |
| Queue / Streams | Redis Streams | Outbox relay, BullMQ 5.76+ |
| Job Queue | BullMQ | Worker entrypoint for async processing |
| Events (in-process) | EventEmitter2 | @OnEvent pattern — migrating to outbox consumers |
| Schema / Validation | Zod 4 | libs/contracts for all event schemas |
| Resilience | Custom circuit breaker | Rate limiter + state management on outbound HTTP |
| Webhook Raw Body | fastify-raw-body |
HMAC-SHA256 signature verification |
| Tenant Context | nestjs-cls |
TenantScopeService carries across process boundaries |
| Proxy / Routing | Caddy | Strangler routing flag, X-CLM-Route header |
| Containerization | Docker | Multi-stage build, GHCR push |
| Contract Testing | Pact | Consumer-driven, self-hosted broker |
| Field Maps | src/common/field-maps/ |
2-3 char abbreviated field names on all Mongo schemas |
- WhatsApp Cloud API for messaging + calling
fastify-raw-bodyfor webhook HMAC-SHA256- Custom rate limiter with circuit-breaker on outbound client
Idempotency-Keyheader pattern for deduplication- Zod 4 schemas for
CallInitiated,CallRinging,CallAnswered,CallEnded,CallMissedinlibs/contracts
- MongoDB Change Streams for outbox relay (planned)
- BullMQ worker with OTEL instrumentation (planned)
SERVICE_MODEenv var for same-image process mode (planned)TenantEnvelopecarryingtenantId,requestId,traceparent,actorIdthrough queues
Every running process talks to one shared system database (Control Plane) and one or more per-tenant databases (Data Plane).
Control Plane — src/modules/platform/
- Wired with
MongooseModule.forRootAsync()insrc/app.module.ts - Stores: platform identities, tenant records, cluster configs, billing plans, currencies, storage providers, courier definitions, Meta Graph API versions, audit logs, alert channels, white-label configs
- Repository pattern:
ControlPlaneBaseRepositoryatsrc/common/repositories/control-plane-base.repository.ts - Every index starts with
tenantIdfor ESR compliance
Data Plane — src/modules/tenant/
- Each tenant gets
tenant_<tenantId>MongoDB database - No
MongooseModule.forFeature— runtime-resolved connection viaTenantScopeService.getConnection() - Stores: orders, products, customers, carts, wishlists, conversations, messages, lifecycle sends, settings, tax rules, coupons
- Direct collection access:
connection.collection('<name>')
ClusterConnectionService at src/modules/platform/cluster-connection/cluster-connection.service.ts:
@Global()provider, caches one MongooseConnectionper cluster- Decrypts connection strings with
CLUSTER_ENCRYPTION_KEYon boot - 15-second cron health check, circuit-breaker state per cluster
- Methods:
getClusterConnection(clusterId),getTenantConnection(tenantId, clusterId),getTenantConnectionByClusterObjectId(tenantId, clusterObjectId) - Tenant DB resolved as
clusterConnection.useDb('tenant_' + tenantId, { useCache: true })
Store: TenantClsStore via TenantScopeService at src/common/services/tenant-scope.service.ts
Guard chain:
JwtAuthGuard(globalAPP_GUARD) — verifies JWT, attachesrequest.userIdentityResolveGuard— readstenantIdfrom JWT, looks upclusterId, writes both to CLS storeTenantGuard— enforces@TenantRequired()/@TenantOptional()decorators
Background jobs: tenantScope.runWithTenant(tenantId, fn, { clusterId }) per job payload.
New Data Plane collections must register in both:
src/seed/shared/seeds/schema-sync.seed.ts— seed-side registrysrc/modules/platform/tenant-provisioning/config/data-plane-collections.config.ts— runtime registry
Forgetting either = new tenants won't get the collection, indexes won't build, reads silently hit empty namespace.
Verified against origin/main at commit 95c85ed.
Path: src/modules/platform/meta-integration/
| Component | File | Status |
|---|---|---|
| Orchestration | meta-integration.service.ts |
DONE |
| Graph API version pinning | meta-api-version.service.ts |
DONE — pinned v25.0 |
| Tenant feature gating | tenant-messaging-feature-gate.service.ts |
DONE |
Path: src/modules/tenant/messaging/shared/
| Component | File | Status |
|---|---|---|
| Graph API client | meta-api.client.ts |
DONE — all channels use this |
| Webhook signature | webhook-signature.util.ts |
DONE — HMAC-SHA256 |
Path: src/modules/tenant/messaging/whatsapp/
| Component | Status | Notes |
|---|---|---|
| Credentials | DONE | Embedded signup + manual connect |
| Outbound messaging | DONE | BullMQ queue, rate limiter, idempotency key |
| Templates | DONE | CRUD + approval status tracking |
| Flows | DONE | Create, update, publish, deprecate |
| Media | DONE | Upload, download, URL resolution |
| Commerce | DONE | Catalog, orders |
| Calling API | DONE | Full implementation: initiate, respond, terminate, webhook events |
| Settings | DONE | Per-tenant WhatsApp config |
| Lifecycle sends | DONE | Order, payment, cart, inventory, courier events |
| Idempotency-Key | DONE | Redis-based dedup on outbound, 24h TTL |
Path: src/modules/tenant/messaging/messenger/
| Component | Status | Notes |
|---|---|---|
| Credentials | DONE | Token storage |
| Outbound queue | DONE | BullMQ |
| Inbound handler | DONE | @OnEvent('meta.webhook.messenger_event') |
| Shared client | MISSING | Uses direct axios, hardcoded v25.0 URL |
Path: src/modules/tenant/messaging/instagram/
| Component | Status | Notes |
|---|---|---|
| Credentials | DONE | Token storage |
| Messaging service | DONE | Uses shared client for credential ops |
| Token refresh | DONE | Scheduled refresh |
| Inbound handler | DONE | @OnEvent('meta.webhook.instagram_event') |
| API client | PARTIAL | Forked InstagramApiClient — duplicates shared client |
Path: src/webhooks/meta/
| Component | Status |
|---|---|
| Controller | DONE — meta-webhook.controller.ts |
| Signature guard | DONE — HMAC-SHA256 enforcement |
| Idempotency | DONE — dedup with SHA-256 payload hashing |
| Persistence | DONE — messaging_webhook_logs collection |
| Tenant resolution | DONE — from payload (phone number ID / page ID / IG business account ID) |
Path: src/modules/tenant/messaging/conversation/
Collections owned: messaging_conversations, messaging_messages, conversation_assignments, customer_channel_mappings
Path: libs/contracts/src/events/
| Schema | Status |
|---|---|
| Call events (6 states) | DONE — discriminated union on type |
| Message received (4 channels) | DONE — discriminated union on channel |
| Tenant envelope | DONE — tenantId + requestId + traceparent + actorId |
src/common/enums/permission-module.enum.ts:
MESSAGING,MESSAGING_TEMPLATES,MESSAGING_FLOWS,MESSAGING_ANALYTICS— DONEWHATSAPP,MESSENGER,INSTAGRAM,CONTACT,LEAD— not yet (messaging treated as single domain)
17 messaging-related maps in src/common/field-maps/. Missing: tenant-contact.field-map.ts, lead.field-map.ts (phase 4).
- Receive inbound WhatsApp messages via Meta webhook
- Verify webhook signature using HMAC-SHA256 (
fastify-raw-body) - Reject tampered/invalid payloads
- Route verified messages into queue pipeline
Status: DONE.
- Send outbound messages via WhatsApp Cloud API
- Per-tenant credential storage (token, phone number, display name, template list)
- Credentials pre-provisioned by Meta business partner; API only stores and uses them
- Circuit-breaker pattern on outbound client for resilience
Idempotency-Keysupport to prevent duplicate sends
Status: DONE (circuit breaker, idempotency key, credential storage all implemented).
- Receive inbound call events from WhatsApp Cloud API
- Initiate outbound calls via WhatsApp Cloud API
- Track call state per conversation (initiated, ringing, answered, missed, ended)
- Publish
Call*events through the event pipeline
Status: DONE. Full implementation verified — not a stub.
- Zod schemas in
libs/contractsfor all call states - Schemas consumed by tenant console (agent click-to-call) and admin console (operator monitoring)
Status: DONE. Schemas in libs/contracts/src/events/call-events.schema.ts.
- REST endpoints for both consoles to connect a tenant to WhatsApp Cloud API
- Accept per-tenant credentials, write to API, surface connection state
- Embedded Signup flow with Meta OAuth callback
Status: DONE. Endpoints: POST /connect/callback, POST /connect/manual, DELETE /disconnect.
- Outbound template messages for outside the 24-hour customer service window
- Per-tenant approved-template list served to tenant console template picker
Status: DONE. Full CRUD with approval status tracking.
- Synthetic webhook canary against staging
- Call-state test matrix against production sandbox
- Full end-to-end integration test
Status: DONE. Test suite at test/messaging-whatsapp-validation/whatsapp-validation.e2e-spec.ts.
- Ingest messages from Messenger, WhatsApp, Instagram, and website widget
- Channel-aware
MessageReceivedZod schema withchanneldiscriminator inlibs/contracts - All channels publish through the same outbox-backed pipeline
Status: PARTIAL. Schemas done in libs/contracts. Handler unification pending — three separate @OnEvent handlers today.
- Inventory all
@OnEventlisteners in messaging tree (~13 files, ~27 handlers) - Migrate replaceable listeners to outbox consumers
Status: PARTIAL. Inventory complete. WooCommerce has outbox pattern as reference. Messaging not yet migrated.
- Every queue message carries:
tenantId,requestId,traceparent,actorId,payload - CI check fails on missing
tenantId
Status: PARTIAL. tenantId present. requestId, traceparent, actorId missing from payloads. Schema defined in libs/contracts.
- OTEL SDK in
main.tsand BullMQ worker entrypoint traceparentpropagation: Caddy → API → Worker
Status: NOT STARTED. No @opentelemetry in codebase.
- Same Docker image,
SERVICE_MODE=messagingenv var - Behind Caddy Strangler routing flag
Status: NOT STARTED. No SERVICE_MODE usage.
- Consumer-side Pact for
MessageReceivedacross all four channels - Pact verification wired into CI
Status: PARTIAL. Pact deps installed (@pact-foundation/pact, nestjs-pact). Health check pact test exists. No messaging contracts.
Status: NOT STARTED.
Status: NOT STARTED.
Client priority order:
- Audit credential, template, outbound flow against Graph API v25.0
- Confirm inbound ingress, signature validation, idempotency, conversation persistence
- #890 (Embedded Signup) — major open feature
Status: Phase 1 audit DONE. All endpoints verified against v25.0. No hardcoded versions found.
- Audit outbound delivery, inbound parsing, signature handling
- Extend shared
meta-api.client.ts— do NOT fork per platform - Migrate Messenger from direct axios to shared client
- Consolidate Instagram forked client into shared client
Status: Audit DONE. Both need shared client migration.
- #892 tracks calling API replacement
- Codebase already has full implementation (verified — not a stub)
- May need issue re-scope or closure
Status: Implementation already DONE. Schema layer added in libs/contracts.
- Open fresh feature issue when phase 1 stabilises
- Design doc at
docs/design/tenant-contacts-lead-funnel.md
Status: Design DONE. Implementation NOT STARTED.
| Day | Deliverable | Status |
|---|---|---|
| 1 | WhatsApp Cloud API integration plan | DONE |
| 2 | Calling-event schemas in libs/contracts |
DONE |
| 3 | Signature verification, tamper-payload test | DONE (existed) |
| 4 | Outbound client, credential storage, channel onboarding | DONE (existed) |
| 5 | Calling integration end-to-end | DONE (existed) |
| 6 | Circuit-breaker, Idempotency-Key support |
DONE |
| 7 | Full sandbox end-to-end | DONE (existed) |
| 8 | Calling tool sandbox validation | DONE (existed) |
| 9 | Integration docs, operations runbook | PARTIAL |
| 10 | Production sandbox verification, test matrix | DONE (validation suite) |
| Day | Deliverable | Status |
|---|---|---|
| 1 | API endpoint, queue, @OnEvent listener map |
DONE (27 handlers across 13 files) |
| 2 | MessageReceived Zod schema with channel discriminator |
DONE |
| 3 | OTEL init in main.ts + worker |
NOT STARTED |
| 4 | Envelope-aware publication across emitters | PARTIAL |
| 5 | Listener migration, Pact consumer for all channels | NOT STARTED |
| 6 | Messaging module as SERVICE_MODE=messaging |
NOT STARTED |
| 7 | Flip staging Strangler flag | NOT STARTED |
| 8 | Load tests, connection-pool tuning | NOT STARTED |
| 9 | Deployment docs, rollback procedure | NOT STARTED |
| 10 | Production traffic verification | NOT STARTED |
| Dependency | Owner | Day | Status |
|---|---|---|---|
Calling-event schemas in libs/contracts |
B1 | 2 | DONE |
MessageReceived schema with channel discriminator |
B2 | 2 | DONE |
| OTEL traces visible end-to-end | B2 | 3 | NOT STARTED |
| Tenant envelope CI check live | Track A | 4 | NOT STARTED |
| WhatsApp onboarding endpoints | B1 | 4 | DONE |
| B1 outbound client ready | B1 | 5 | DONE |
| Messaging service as separate process | B2 | 6 | NOT STARTED |
| Pact broker accepts contracts | Track A | 7 | PARTIAL |
| Messaging service serves staging traffic | B2 | 7 | NOT STARTED |
The tenant_contacts collection (phase 4, greenfield).
Inbound message → resolve/create tenant_contact → attach to messaging_conversation
→ sales/automation promotes → customers collection + customer_channel_mappings
Full design doc: docs/design/tenant-contacts-lead-funnel.md
Key points:
- Data Plane collection:
tenant_contacts - Status lifecycle:
new→engaged→qualified→converted_to_customer - Must register in
schema-sync.seed.ts+data-plane-collections.config.ts - Needs
tenant-contact.field-map.tsinsrc/common/field-maps/ - Permission modules:
CONTACT,LEADinpermission-module.enum.ts - 4 indexes: dedup, status, converted, recency
- 6 API endpoints under
/api/v1/messaging/contacts
New client = new tenant. Steps:
- Admin creates tenant — Control Plane endpoint creates tenant record with
clusterId,status, billing plan - Tenant DB provisioned —
TenantProvisioningService.provisionTenantDatabase()creates collections + indexes - Company settings seeded — Default feature toggles, security settings, localization
- Tenant identity created — Admin or invitation system creates tenant-scoped user, assigns roles
- WhatsApp channel connected — Embedded Signup (
POST /messaging/whatsapp/connect/callback) or manual (POST /messaging/whatsapp/connect/manual) - Webhook subscription — App subscribes to WABA webhooks
- Templates synced — WhatsApp templates fetched from Meta
- Lifecycle rules configured — Enable/disable auto-messages for order events
- Zod-only validation — No
class-validator. DTOs viacreateZodDto()fromnestjs-zod - Field shortening — Every new Mongoose schema uses 2-3 char abbreviated names with
alias. Runbun run validate:field-maps - Multi-tenancy split —
src/modules/platform/= Control Plane (shared DB).src/modules/tenant/= Data Plane (per-tenant DB) - Response format —
ResponseHelper.success(). Errors follow RFC 7807. API prefix:/api/v1/* - No caching decorators —
@Cacheable/@CacheEvictforbidden. Rely on indexes +lean() - Bangladesh-only — BDT,
+880phone, divisions/districts/upazilas. No GDPR/CCPA/SOC2/HIPAA/PCI-DSS - Fixed domains —
endapis.com,api.endapis.com,cdn.endapis.com - No code comments — No
console.*. No debug residue. No auto-commits without instruction - Enum location — All enums in
src/common/enums/. Import from@enums
# Prerequisites: Bun >=1.3.8, MongoDB 8.x
# Install
bun install
chmod +x .husky/pre-commit .husky/pre-push
# Environment (bare minimum)
cat > .env << 'EOF'
NODE_ENV=development
APP_PORT=5000
MONGO_URI=mongodb://localhost:27017/clm-api
CORS_ORIGINS=http://localhost:3000
EOF
# Start MongoDB (Docker)
docker run -d --name mongodb -p 27017:27017 mongo:8
# Seed
bun run seed:dev
# Boot
bun run start:dev
# Verify
curl http://localhost:5000/health/ping
# {"status":"ok","message":"pong"}Test accounts created by seed:
| Role | Password | |
|---|---|---|
| Developer | developer@endapis.com |
Dev$3cur3!Pl@tf0rm |
| Authority | authority@endapis.com |
Auth#C0ntr0l!2025 |
| Super Admin | superadmin@endapis.com |
Sup3r@Adm1n!Acc3ss |
| Client Admin | clientadmin@endapis.com |
Cl13nt#Adm1n!M@n@g3 |
| Client User | clientuser@endapis.com |
Cl13nt$Us3r!Acc0unt |
Paths verified on origin/main at commit 95c85ed.
docs/setup/DEVELOPER-SETUP.md,docs/setup/ENVIRONMENT-VARIABLES.md.env.exampleat repo rootsrc/config/env.validation.ts,src/config/database.config.ts,src/config/mongodb-connection-options.config.ts
src/app.module.tssrc/common/services/tenant-scope.service.tssrc/modules/platform/cluster-connection/cluster-connection.service.tssrc/modules/platform/cluster-config/infrastructure/persistence/schemas/cluster-config.schema.tssrc/common/repositories/control-plane-base.repository.tssrc/common/guards/identity-resolve.guard.ts,src/common/guards/tenant.guard.tssrc/common/decorators/tenant.decorator.tssrc/modules/platform/tenant-provisioning/tenant-provisioning.service.tssrc/modules/platform/tenant-provisioning/tenant-schema-migration-bootstrap.service.tssrc/modules/platform/tenant-provisioning/config/data-plane-collections.config.tssrc/modules/platform/tenant-provisioning/config/control-plane-collections.config.tssrc/seed/shared/seeds/schema-sync.seed.tssrc/modules/platform/domain-resolution/application/domain-resolution.service.tssrc/worker.ts,src/worker.module.ts
src/modules/platform/meta-integration/src/modules/tenant/messaging/shared/meta-api.client.ts,webhook-signature.util.tssrc/modules/tenant/messaging/whatsapp/src/modules/tenant/messaging/messenger/src/modules/tenant/messaging/instagram/src/modules/tenant/messaging/conversation/src/webhooks/meta/src/webhooks/woo-commerce/(architectural reference)src/common/field-maps/src/common/enums/permission-module.enum.ts
libs/contracts/src/events/call-events.schema.tslibs/contracts/src/events/message-events.schema.tslibs/contracts/src/events/envelope.schema.ts
- #890 — WhatsApp Embedded Signup
- #892 — Calling API replacement (may need re-scope — already implemented)
- #1174 — Onboarding map (this document)
clm-infra(git@github.com:Goclmbd/clm-infra.git) — Docker Compose, Caddy, deploymentclm-tenant-console,clm-admin-console— Next.js frontends