Overview
A production-grade customer support platform is far more complex than a shared inbox. It must ingest messages from heterogeneous channels, normalise them into a unified ticket model, route them intelligently, enforce SLA contracts in real time, and maintain a tamper-proof audit trail - all while staying responsive under burst load.
This guide walks through the architectural decisions behind IdeaSprout's Customer Support module.
1. Multi-Channel Ingestion Layer
The Problem
Customers contact you via email, WhatsApp, web chat, phone (voice-to-text), and social DMs. Each channel has a different payload format, authentication model, and delivery guarantee.
Architecture Pattern: Channel Adapters + Normalisation Queue
Each channel has a dedicated adapter service responsible for:
- •Authenticating inbound webhooks (HMAC signature verification for WhatsApp Business API, DKIM for email)
- •Parsing the raw payload into a canonical
InboundMessageschema - •Publishing to a central ingestion queue (Apache Kafka topic:
raw.inbound.messages)
WhatsApp Webhook → WhatsApp Adapter → Kafka: raw.inbound.messages
Email (IMAP/SMTP) → Email Adapter → Kafka: raw.inbound.messages
Web Chat (WS) → Chat Adapter → Kafka: raw.inbound.messagesCanonical InboundMessage Schema
{
"messageId": "uuid-v4",
"channel": "whatsapp | email | chat | phone",
"customerId": "resolved or null",
"contactIdentifier": "+91-XXXXXXXXXX | [email protected]",
"body": "plain text content",
"attachments": [],
"receivedAt": "ISO-8601",
"rawPayload": "base64-encoded original"
}Key Design Decisions
- •Idempotency: Each adapter deduplicates using
messageIdbefore publishing. WhatsApp re-delivers webhooks on timeout - without dedup, you get duplicate tickets. - •At-least-once delivery: Kafka guarantees at-least-once. The ticket creation consumer is idempotent (upsert on
messageId). - •Customer resolution: A separate
CustomerResolverservice enriches the message withcustomerIdby looking up the contact identifier against the CRM. If unresolved, the ticket is created withcustomerId: nulland flagged for manual association.
2. Ticket Creation & Deduplication Engine
Thread Detection
Replies to existing tickets must be appended to the thread, not create new tickets. The engine uses:
- 1Email:
In-Reply-ToandReferencesheaders → match againstticket.emailThreadId - 2WhatsApp:
context.idfield in the webhook payload → match againstticket.whatsappThreadId - 3Fallback: Fuzzy match on
customerId+ recency window (last open ticket within 24h from same customer)
Ticket State Machine
NEW → OPEN → PENDING (awaiting customer) → RESOLVED → CLOSED
↓
ESCALATED → OPEN (reassigned)State transitions are enforced server-side. Invalid transitions (e.g., CLOSED → OPEN without explicit reopen) are rejected with a 422.
3. Intelligent Routing Engine
Routing Rules Evaluation Order
- 1Keyword rules - regex match on ticket body (e.g., "refund" → Billing queue)
- 2Channel rules - WhatsApp tickets → WhatsApp-trained agents
- 3Customer tier rules - Enterprise customers → Senior agents
- 4Round-robin - within the matched queue, distribute evenly by agent load score
Agent Load Score
load_score = (open_tickets × 1.0) + (pending_tickets × 0.3) + (escalated_tickets × 2.0)The agent with the lowest load score in the target queue receives the ticket. Scores are recomputed on every assignment event.
Overflow Handling
If all agents in a queue exceed max_load_score (configurable, default: 15), the ticket enters a backlog queue and triggers a Slack/email alert to the queue manager.
4. SLA Engine
The Core Challenge
SLA timers must pause when the ball is in the customer's court (ticket in PENDING state) and resume when the customer replies. This requires business-hours-aware elapsed time calculation.
Timer Architecture
Each ticket has two SLA clocks:
- •First Response Time (FRT): Time from ticket creation to first agent reply
- •Resolution Time (RT): Total agent-active time from creation to RESOLVED
Both clocks are stored as cumulative elapsed seconds, not as absolute deadlines. This makes timezone changes and business-hours reconfiguration non-destructive.
CREATE TABLE sla_events (
ticket_id UUID,
event_type ENUM('start', 'pause', 'resume', 'breach'),
occurred_at TIMESTAMPTZ,
elapsed_ms BIGINT -- cumulative at this point
);Breach Detection
A scheduled job runs every 60 seconds and evaluates:
current_elapsed = last_elapsed + (now - last_resume_at) × business_hours_factor
if current_elapsed > sla_threshold → emit SLA_BREACH eventSLA breach events trigger escalation rules (reassign, notify manager, change priority).
5. CSAT Collection Pipeline
Trigger
CSAT surveys are sent 2 hours after a ticket moves to CLOSED state (configurable delay). The survey is sent via the same channel the customer used.
Anti-Spam Guard
- •Maximum 1 CSAT survey per customer per 7 days
- •If a customer has multiple tickets closed in the same window, only the most recent triggers a survey
Score Aggregation
CSAT scores are written to a time-series table partitioned by (product_id, date). The dashboard queries a materialised view that recomputes rolling 7-day and 30-day averages on a 15-minute schedule - avoiding expensive full-table scans on every dashboard load.
6. Audit Trail & Data Integrity
Append-Only Event Log
Every state change, assignment, reply, and SLA event is written to an append-only event log table. The current ticket state is derived by replaying events (event sourcing pattern) or read from a denormalised snapshot table that is updated by event consumers.
This means:
- •You can reconstruct the full history of any ticket at any point in time
- •Deleted or edited messages are detectable (the original is preserved in the log)
- •Compliance audits can be satisfied by exporting the event log
Data Retention
- •Active tickets: hot storage (PostgreSQL)
- •Closed tickets > 90 days: cold storage (S3 + Parquet), queryable via Athena
- •Event log: retained for 7 years (regulatory requirement for customer communications in India)
7. Common Architectural Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Storing SLA as absolute deadline | Timezone bugs, business-hours changes break all open tickets | Store as elapsed seconds |
| No deduplication on webhook ingestion | Duplicate tickets from WhatsApp re-delivery | Idempotent upsert on messageId |
| Routing computed at query time | Dashboard slowness under load | Pre-compute load scores on assignment events |
| CSAT sent immediately on close | Customer hasn't processed resolution yet, scores are lower | 2-hour delay |
| Mutable ticket body | Audit trail gaps, compliance risk | Append-only message log |
Summary
A robust customer support platform requires careful design at every layer: idempotent ingestion, thread-aware deduplication, load-balanced routing, business-hours-aware SLA timers, and an append-only audit trail. Cutting corners on any of these leads to duplicate tickets, SLA miscalculations, or compliance failures that are expensive to fix retroactively.