A production platform that ingests Meta webhooks, routes conversations across projects and queues, sends through a resilient outbox, and layers AI agents, chatbots, templates, mass sends, calls and number-health analytics on top — built on four .NET 8 services and one SQL Server database.
A support operation was spread across five legacy messaging providers, single-channel tools, and a database of 200+ tables where hot reads scanned tens of millions of log rows. Agents couldn't see WhatsApp, Instagram and Messenger in one place; there was no safe way to initiate conversations inside Meta's 24-hour window, no controlled mass sending, no number-health guardrails against Meta bans, and AI/classification lived in a separate database the runtime depended on.
The goal: consolidate everything onto Meta only, multi-project and multi-agent, without losing a single production value.
Webhooks enter through a narrow ingestion boundary; the API reads and writes intent; the Worker is the only thing that talks outbound to Meta.
ASP.NET Core (.NET 8), raw ADO.NET with stored procedures only (no ORM). GET /webhook answers Meta's hub verification; POST /webhook accepts events. A middleware captures the raw request bytes so X-Hub-Signature-256 HMAC can be validated and a SHA-256 event hash computed. The controller returns HTTP 200 in under ~200 ms; events land in an in-memory ConcurrentQueue and a background worker flushes them to SQL every 5 s or at 100 events via a batch insert. Idempotent on event hash + provider message id, so duplicate deliveries are ignored.
.NET 8 Web API. Owns authentication, roles and permissions, project / account / queue selection, chat reads and writes, notes / tags / locks / search, templates, calls, dashboards and AI tools. It never calls Meta directly for sends — it writes a request into the outbox. Reads are served by purpose-shaped stored procedures and cached aggressively; writes drop cache-invalidation rows. A SignalR hub pushes new messages, status changes and AI actions to the front in real time.
.NET 8 worker service. Drains the outbox and performs all outbound Meta calls: messages, media (downloaded/uploaded through the existing S3 store), template creation and status sync, chatbot runtime, classification jobs, and a dedicated mass-send loop separate from one-to-one traffic. It also snapshots number-health signals and purges ephemeral internal chat older than 5 days. Concurrency uses lease columns (LockedBy, LockedUntilUtc, AttemptCount, NextAttemptAtUtc) — no cursors, no table types, no triggers, no explicit transactions — with an explicit retry and dead-letter policy.
Blazor Server with a SignalR live connection. A dark-first design system (IBM Plex, AA-tuned in light and dark, compact operational density) drives a three-panel chat console plus dashboards, AI agents, chatbots, templates, mass sends, calls and internal chat. Module visibility is gated per project by feature flags, permission and readiness. (The prototype reproduces this UI as a static React app.)
Everything lives in a single SQL Server database, organized into schemas by domain so hot reads stay narrow and legacy logs stay out of the path: core (projects, users, roles, queues), meta (apps, tokens, WABA, accounts, raw + normalized events, templates), chat (contacts, conversations, messages, media, status history, notes, tags, locks), bot, classify, mass, calls, internal, ops (outbox, leases, health, dashboard snapshots) and legacy (id maps only). The Meta webhook tables were preserved as the ingestion boundary; new normalized tables were added additively with an audit-friendly backfill that keeps every legacy id.
One conversation list across every channel and queue, a live message thread with delivery/read ticks, and an operations rail for locks, tags, notes, classification, AI state and growing customer context. Optimistic sends, lazy media, pin-to-top, advanced search.
Configurable autonomous agents per project, authored in a tabbed editor: persona/prompt, what to measure, tools, handoff rules and a RAG manual. Agents answer, classify, suggest and, when they hit a boundary, open a human action and hand the chat off. Multimodal input: vision, audio transcription (Whisper) and documents.
Migrated node-based chatbots activatable per Meta account, with reply buttons, expected answers, keyword→intent routing and per-chat sessions. If a human agent types into a chat with an active bot, the bot is disabled for that conversation immediately — an auditable state.
Project/account-scoped Meta templates with structured header/body/footer/button preview, live variable substitution, and AI autofill from customer context. When Meta's 24-hour service window is closed, free-form is blocked and only an approved template can continue the conversation — enforced in the composer.
Campaigns scoped to a project/account/template, with audience upload, rate-limit policy, an approval gate and per-recipient results with the Meta message id. A dedicated worker loop runs independently — and stops automatically when a number's health hits critical risk.
Call permissions per agent/account/project, an active-call timeline of who's calling whom, presence, and a switchboard view restricted to admins and devs. Recording metadata is modeled with S3 links where feasible.
A project-local, Meta-independent chat for agents, visually distinct (dashed bubbles, diagonal texture). Messages can carry a card that links straight to a specific customer conversation. Retention is 5 days, purged by the worker — never by a trigger.
Metrics organized Data → Knowledge → Wisdom: per-agent configurable activity tiles, a sales funnel, per-channel comparison, and AI-generated insights over aggregates. A health score per Meta number combines delivery failures, read-rate trends, opt-outs, send volume and template quality — computed from snapshots, never by scanning millions of raw rows.
HTTPS + HMAC-validated webhooks, restricted admin endpoints, encrypted secrets kept out of source, an auditable command log for developer/admin actions, worker heartbeats and an error log. Secrets never appear in migration scripts.
The live webhook receiver stayed untouched; new normalized tables were layered beside it so ingestion never broke during migration.
Queue work is claimed with LockedBy / LockedUntilUtc and retried with NextAttemptAtUtc, keeping the DB auditable and the logic in the app.
The API never blocks on Meta and never risks a partial send; the Worker is the single, retryable sender.
Domain schemas + denormalized latest-message fields on the conversation keep the inbox fast over millions of historical rows.
The API caches catalogs, chat pages and permissions; writes emit invalidation rows so the front stays fresh without re-querying everything.
Backfills are idempotent and keep legacy ids, so new data traces back to old data and can re-run safely.
Durable AI/classification config was brought into the platform database; the runtime calls OpenAI directly and logs tokens/latency, with no runtime dependency on an external DB.
A single Meta-only platform where multiple projects and many agents operate WhatsApp, Instagram and Messenger from one console — with AI agents and chatbots handling first contact, safe conversation initiation inside Meta's rules, controlled mass sends with health guardrails, calls, ephemeral internal chat, and dashboards that turn raw traffic into number-health decisions. Migrated from 200+ legacy tables without losing a production value, and without ever breaking the live webhook receiver.
A non-functional prototype of the operator console — 8 screens with sample data, dark theme, bilingual.
Open the live prototype →