Design Mobile Chat Client Architecture
1. Problem Statement & Scope
System Mission
Design an ultra-responsive, battery-optimized, offline-resilient mobile chat client architecture capable of managing bidirectional persistent WebSocket connections, SQLCipher-encrypted local storage, Signal Protocol (Double Ratchet) end-to-end encryption (E2EE), compact binary Protocol Buffers serialization, resumable binary media streaming, and silent background push synchronization across iOS and Android platforms.
Hardware Key Isolation Invariant: The SQLCipher database key and the device credential key are generated inside and never leave the hardware security chip (Apple Secure Enclave / Android KeyStore StrongBox); everything cryptographic that must live on flash (identity key, Double Ratchet root/chain keys, skipped message keys) is stored only inside the SQLCipher database that this hardware-wrapped key unlocks. Ratchet keys are necessarily decrypted transiently in process memory while a message is encrypted or decrypted (the Secure Enclave only performs P-256 operations, so Curve25519 ratchet math cannot run inside it) and are zeroized immediately afterwards. This bounds an attacker on a rooted device to the live process, never to backups, USB extraction, or the raw database file.
Synthesizing vector architecture diagram...
Functional Requirements
-
Real-Time 1:1 and Group Chat: Send and receive text, voice memos, media attachments, reactions, and threaded replies over low-latency binary WebSockets ().
-
Offline-First Messaging & Local Outbox: Messages authored offline immediately persist to encrypted local storage (SQLCipher) in optimistic state (
DRAFT/SENDING) and render in the UI with a pending clock badge, draining sequentially upon reconnect. -
Granular Message Delivery State Machine: Track delivery states deterministically:
-
Hardware-Backed End-to-End Encryption (E2EE): Execute the Double Ratchet (Signal Protocol) cryptographic state machine with root and identity keys anchored in device hardware security modules (Apple Secure Enclave / Android KeyStore).
-
Adaptive WebSocket Lifecycle & Battery Management: Maintain persistent socket connections with adaptive heartbeats ( WiFi, cellular LTE/5G), suspending connections when backgrounded to eliminate battery drain.
-
Silent Push Synchronization Fallback: On missed messages or when the app is backgrounded, Apple APNs and Google FCM deliver data-only silent pushes (
content-available: 1), waking a background worker to fetch missing message windows from DynamoDB. -
Resumable Binary Media Uploads: Upload multi-megabyte images, videos, and documents via SHA-256 chunk hashing, presigned S3 URLs, and background OS upload sessions (
NSURLSession/ AndroidWorkManager).
Non-Functional Requirements (SLAs/SLOs)
- Send-to-Display Latency: roundtrip latency on 4G/5G networks; instant local optimistic UI insertion.
- Battery & Radio Footprint: total device battery consumption per 24 hours under normal conversational usage; radio dormancy honored via adaptive heartbeats.
- Local Database Encryption: AES-256 encryption at rest (SQLCipher) covering messages, attachments, participants, and cryptographic session keys.
- Network Wire Efficiency: payload size reduction achieved via binary Protocol Buffers compared to standard JSON over WebSockets.
- Reconnection Recovery Time: Session resumption () upon network switch (cellular to WiFi handoff).
- CAP / PACELC Classification: The mobile client operates as an AP node prioritizing local Availability and low Latency (PA/EL). Backend chat routing enforces causal consistency per conversation.
Out-of-Scope
- Peer-to-peer WebRTC voice/video mesh calling (covered in dedicated VoIP blueprints).
- Broadcast channel feeds with concurrent subscribers.
2. Capacity & Scale Estimation
Device & Traffic Scale
- Active User Base: 100 Million registered accounts; 30 Million Daily Active Users (DAU).
- Concurrent Connected Sockets: Peak 10 Million simultaneous active WebSocket connections.
- Daily Message Volume: 500 Million messages per day.
- Delivery & Read Receipts Volume: Each message generates 2 receipt frames (1 delivery receipt + 1 read receipt):
Bandwidth & Serialization Efficiency
- Wire Serialization Comparison:
- Standard JSON Frame: (quoted keys, four 36-char UUID strings, base64-encoded 32-byte ephemeral key, 16-byte tag and ciphertext).
- Optimized Protobuf Binary Frame: for a short text message ( bandwidth reduction). Byte budget: 4 × 16-byte binary IDs (72 B with tags), 32-byte ephemeral key (34 B), 16-byte auth tag (18 B), varint timestamp/status/counter (~13 B), plus the ciphertext itself (~20-30 B for a short message). A frame cannot be smaller than its fixed key material, which is why the IDs are
bytes, not UUID strings.
- Daily Wire Throughput:
- Adaptive Heartbeat Bandwidth:
10 Million clients sending a ping every 120 seconds (blended average between 60s WiFi and 180s LTE). The
HeartbeatPingpayload is ~11 bytes, but on the wire each ping costs once the WebSocket frame header, TLS record and TCP/IP headers are added, and the pong doubles it: - Resumable Media Uploads: of messages include attachments (50 Million files/day). Average compressed file size .
Cloud Storage Calculations (3-Year Horizon)
- Message Storage (DynamoDB Mailbox): 500 Million messages/day over 3 years . Record footprint encrypted ciphertext and routing metadata. Replicated across 3 Availability Zones: . DynamoDB TTL auto-expires delivered mailbox messages after 90 days ( active set), archiving older messages to Amazon S3 Glacier.
- ElastiCache Redis Session Routing Memory:
10 Million active connections mapping
user_id -> { gateway_id, connection_id }@ 128 Bytes . Session pub/sub channels and hot metadata cache: .
Fleet Sizing & Compute Infrastructure
- WebSocket Gateway Envoy Fleet (ECS Fargate / EC2): 10 Million concurrent connections. Each Envoy proxy container handles 50,000 persistent multiplexed TCP sockets.
- Chat Router & Dispatch Workers: Peak 20,000 message QPS + 40,000 receipt QPS . Each stateless router task handles 1,500 operations/sec .
3. AWS-First High-Level Architecture
Synthesizing vector architecture diagram...
Follow a message from one phone to another. The sender's phone first encrypts it on the device (see the "Mobile Client Storage & Encryption Engine" panel: Signal Double Ratchet keys held in the Secure Enclave or KeyStore, local database encrypted with SQLCipher). It travels over a persistent WebSocket through the NLB to an Envoy proxy in the "Persistent WebSocket Edge Tier" panel. In the "Stateless Chat Router & Session Tier" panel, the router looks up the recipient's live connection in Redis. The "Durable Storage & Mailbox Tier" panel stores the still-encrypted message in the recipient's mailbox and updates read positions, and prekey bundles let new conversations start while the other person is offline. If the recipient is offline, the "Media & Background Push Tier" panel sends a silent push that wakes the app to sync, and media uploads go straight to S3 in resumable chunks. The server only routes and stores ciphertext; it can never read messages.
Data Flow Architecture
- Connection Establishment: When the app foregrounds, the connection manager resolves DNS via Route 53 and initiates a secure WebSocket (
wss://) through Network Load Balancer to the Envoy edge proxy fleet. Envoy validates the JWT token and writes the active session mapping (user_id -> connection_id) to ElastiCache Redis. - Optimistic Outbound Message Path: Alice types and sends; the client assigns a monotonic UUIDv7
message_id, executes the Double Ratchet step to derive a fresh message key, commits ciphertext and plaintext to local SQLCipher with statusSENDING, and renders instantly () in the UI. - Cloud Routing & Real-Time Delivery: The client streams a ~160-byte Protobuf
ChatMessageFrameover the WebSocket. The router persists the frame to DynamoDB, checks Redis for Bob's active connection, and pushes the binary frame down Bob's active Envoy socket in . - Offline / Background Silent Push Path: If Bob is offline or backgrounded, the router publishes a silent push notification via Amazon SNS to APNs/FCM (
content-available: 1). If unacknowledged within 15 seconds, the backend automatically escalates to a high-priority alert notification channel. - Resumable Binary Media Path: Large media attachments bypass the WebSocket entirely. The client requests a Presigned S3 Multipart Upload session, streams 5MB SHA-256 validated chunks directly to Amazon S3 via background OS sessions, and transmits only the finalized media pointer over the WebSocket stream.
End-to-End Request Tracing Walkthrough
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| Step 1 | App foregrounded / network recovery | Socket DISCONNECTED | Connection manager resolves DNS via Route 53; begins WSS TLS 1.3 handshake | TCP socket open; TLS session negotiated () |
| Step 2 | Edge gateway authentication | Socket CONNECTING | Envoy validates JWT bearer token; sets multiplexed TCP stream | Socket promoted to CONNECTED; heartbeat timer armed |
| Step 3 | Active session registry binding | Chat Router active | Router writes active session user_id -> {gateway_id, connection_id} to Redis | Redis session registered with 10-minute TTL heartbeat |
| Step 4 | User types message & taps Send | Chat UI input active | Atomic local write: plaintext & ciphertext committed to SQLCipher as SENDING | UI renders grey clock icon immediately ( latency) |
| Step 5 | Double Ratchet step & Protobuf stream | Crypto engine executing | Generates ephemeral key, steps sending ratchet; streams ~160B ChatMessageFrame | Binary frame in-flight over persistent WebSocket |
| Step 6 | Router mailbox persistence | Chat Router receiving | Conditional put MSG#<timestamp>#<msg_id> into DynamoDB Mailbox table | Exactly-once storage committed; Router prepares delivery |
| Step 7 | Online socket fanout vs push | Router routing | Router checks Redis for recipient session; pushes to recipient's Envoy task | Recipient live: frame routed (); Offline: SNS silent push |
| Step 8 | Recipient background decrypt | Recipient device waking | Background worker wakes (content-available: 1), fetches frame, decrypts via Double Ratchet, commits to SQLCipher | Message committed to local DB; heads-up banner displayed |
| Step 9 | Delivery receipt watermark emission | Recipient socket active | Recipient emits ReceiptBatchFrame(receipt_type=DELIVERED); batched 3s window | Sender client marks message DELIVERED (double grey checks) |
| Step 10 | Recipient reads conversation | Viewport intersection | Recipient scrolls to message; emits ReceiptBatchFrame(receipt_type=READ, watermark) | Sender client marks message READ (double blue checks) |
4. API Interface Design & Wire Protocols
1. Protocol Buffers Wire Schema (chat_wire_protocol.proto)
Binary serialization protocol engineered for ultra-compact payload delivery over persistent WebSockets:
protobufsyntax = "proto3"; package chat.mobile.v1; enum DeliveryStatus { STATUS_UNKNOWN = 0; DRAFT = 1; SENDING = 2; SENT = 3; DELIVERED = 4; READ = 5; FAILED = 6; } message ChatMessageFrame { bytes message_id = 1; // UUIDv7 as 16 raw bytes (never a 36-char string on the wire) bytes conversation_id = 2; // Target conversation (16 bytes) bytes sender_id = 3; // Sender account UUID (16 bytes) bytes recipient_id = 4; // Target recipient UUID (16 bytes) int64 timestamp_ms = 5; // Client submission timestamp DeliveryStatus status = 6; // Current lifecycle state bytes encrypted_payload = 7; // Double Ratchet AES-256-GCM ciphertext bytes ephemeral_public_key = 8; // Ephemeral Curve25519 public key (32 bytes) int32 ratchet_sequence_num = 9; // Monotonic ratchet counter bytes hmac_auth_tag = 10; // 16-byte authentication tag string media_attachment_id = 11; // Optional S3 media pointer } message ReceiptBatchFrame { string conversation_id = 1; DeliveryStatus receipt_type = 2; // DELIVERED or READ repeated string message_ids = 3; // Batched message identifiers int64 watermark_timestamp_ms = 4; // Watermark up to which messages are read } message HeartbeatPing { int64 client_timestamp_ms = 1; bool is_metered_cellular = 2; } message HeartbeatPong { int64 server_timestamp_ms = 1; int32 recommended_heartbeat_interval_sec = 2; }
2. Resumable S3 Media Upload API (POST /v1/media/upload/init)
httpPOST /v1/media/upload/init HTTP/1.1 Host: media.chat.production.aws.internal Content-Type: application/json Authorization: Bearer <jwt_access_token> X-Client-Platform: ios-arm64 { "conversation_id": "conv_998124a", "media_type": "VIDEO_MP4", "total_bytes": 48293400, "chunk_size_bytes": 5242880, "sha256_checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }
Response: 201 Created
json{ "media_id": "media_881293a", "upload_id": "aws_multipart_upload_id_778129", "chunk_size_bytes": 5242880, "total_chunks": 10, "presigned_part_urls": [ "https://s3.us-east-1.amazonaws.com/chat-media-prod/media_881293a?partNumber=1&uploadId=aws_multipart_upload_id_778129&X-Amz-Signature=...", "https://s3.us-east-1.amazonaws.com/chat-media-prod/media_881293a?partNumber=2&uploadId=aws_multipart_upload_id_778129&X-Amz-Signature=..." ], "expires_in_seconds": 3600 }
3. Status Codes & Error Contracts
| Error Code | Reason String | Error Contract Payload | Client Handling / Recovery Strategy |
|---|---|---|---|
200 OK | ACK_RECEIVED | {"status": "ACK", "message_id": "..."} | Advance outbox message to SENT state in SQLCipher |
400 Bad Request | INVALID_PROTOBUF | {"error": "Malformed binary payload"} | Mark message FAILED; display red exclamation icon |
401 Unauthorized | SESSION_REVOKED | {"error": "Auth token expired or revoked"} | Disconnect WebSocket; trigger background OAuth refresh |
409 Conflict | RATCHET_DESYNC | {"error": "Ephemeral key mismatch detected"} | Initiate cryptographic Prekey Bundle re-negotiation |
413 Payload Large | ATTACHMENT_LIMIT | {"error": "Direct socket frame exceeds 64KB"} | Force media upload through Resumable S3 Multipart API |
429 Too Many Req | SOCKET_THROTTLED | {"error": "Exceeded 50 msgs/sec", "backoff": 2} | Buffer outgoing messages in local SQLite; throttle send loop |
503 Service Unav | ROUTER_OVERLOAD | {"error": "Chat router draining connections"} | Disconnect and reconnect with full jitter exponential backoff |
5. Data Models & Storage Architecture
Local SQLCipher Schema (encrypted_chat_store.db)
Encrypted using 256-bit AES-CBC via SQLCipher. Master database passphrase is dynamically derived using PBKDF2 with salt stored in the platform Secure Enclave / Android KeyStore.
sql-- Conversations Table CREATE TABLE conversations ( conversation_id TEXT PRIMARY KEY NOT NULL, peer_user_id TEXT NOT NULL, is_group INTEGER NOT NULL DEFAULT 0, title TEXT, last_message_id TEXT, last_message_preview TEXT, unread_count INTEGER NOT NULL DEFAULT 0, read_watermark_timestamp INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); -- Participants Table (Group Chat Membership) CREATE TABLE participants ( conversation_id TEXT NOT NULL, user_id TEXT NOT NULL, display_name TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('ADMIN', 'MEMBER')), joined_at INTEGER NOT NULL, PRIMARY KEY (conversation_id, user_id), FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id) ON DELETE CASCADE ); -- Messages Table with Full Lifecycle Tracking CREATE TABLE local_messages ( message_id TEXT PRIMARY KEY NOT NULL, -- UUIDv7 conversation_id TEXT NOT NULL, sender_id TEXT NOT NULL, recipient_id TEXT NOT NULL, plaintext_body TEXT NOT NULL, delivery_status TEXT NOT NULL CHECK (delivery_status IN ('DRAFT', 'SENDING', 'SENT', 'DELIVERED', 'READ', 'FAILED')), attachment_id TEXT, is_outgoing INTEGER NOT NULL DEFAULT 1, -- 1 = outgoing, 0 = incoming sequence_number INTEGER NOT NULL DEFAULT 0, timestamp_ms INTEGER NOT NULL, FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id) ON DELETE CASCADE, FOREIGN KEY (attachment_id) REFERENCES attachments(attachment_id) ); CREATE INDEX idx_msg_timeline ON local_messages(conversation_id, timestamp_ms DESC); CREATE INDEX idx_msg_outbox ON local_messages(delivery_status, timestamp_ms ASC); -- Attachments Table for Resumable Uploads & GC Lifecycle CREATE TABLE attachments ( attachment_id TEXT PRIMARY KEY NOT NULL, conversation_id TEXT NOT NULL, file_path TEXT NOT NULL, media_type TEXT NOT NULL CHECK (media_type IN ('IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT')), file_size_bytes INTEGER NOT NULL, sha256_hash TEXT NOT NULL, upload_id TEXT, -- S3 Multipart Upload ID upload_state TEXT NOT NULL CHECK (upload_state IN ('PENDING', 'UPLOADING', 'UPLOADED', 'FAILED')), uploaded_chunks INTEGER NOT NULL DEFAULT 0, total_chunks INTEGER NOT NULL, s3_url TEXT, created_at INTEGER NOT NULL, last_chunk_at INTEGER NOT NULL ); -- Attachment Staged Chunks Table for Garbage Collection Cleanup CREATE TABLE attachment_chunks ( chunk_id TEXT PRIMARY KEY NOT NULL, attachment_id TEXT NOT NULL, chunk_index INTEGER NOT NULL, temp_file_path TEXT NOT NULL, chunk_size_bytes INTEGER NOT NULL, etag TEXT, state TEXT NOT NULL CHECK (state IN ('STAGED', 'UPLOADED', 'ORPHANED')), created_at INTEGER NOT NULL, FOREIGN KEY (attachment_id) REFERENCES attachments(attachment_id) ON DELETE CASCADE ); CREATE INDEX idx_chunks_cleanup ON attachment_chunks(state, created_at ASC); -- Signal Protocol Double Ratchet Cryptographic State CREATE TABLE ratchet_sessions ( conversation_id TEXT PRIMARY KEY NOT NULL, peer_user_id TEXT NOT NULL, root_key BLOB NOT NULL, sending_chain_key BLOB NOT NULL, receiving_chain_key BLOB NOT NULL, ephemeral_public_key BLOB NOT NULL, ephemeral_private_key BLOB NOT NULL, sending_counter INTEGER NOT NULL DEFAULT 0, receiving_counter INTEGER NOT NULL DEFAULT 0, last_rekey_timestamp INTEGER NOT NULL ); -- Skipped Message Keys Cache for Out-of-Order Message Decryption CREATE TABLE skipped_message_keys ( conversation_id TEXT NOT NULL, ratchet_public_key BLOB NOT NULL, sequence_number INTEGER NOT NULL, message_key BLOB NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, -- 30-day bounded TTL (Max 2,000 keys) PRIMARY KEY (conversation_id, ratchet_public_key, sequence_number) ); CREATE INDEX idx_skipped_keys_expiry ON skipped_message_keys(expires_at ASC);
Backend DynamoDB Single-Table Schema (ProductionChatStore)
| Partition Key (PK) | Sort Key (SK) | GSI1PK | GSI1SK | Entity Attributes |
|---|---|---|---|---|
USER#<user_id> | CONN#<connection_id> | GW#<gateway_id> | PING#<epoch_ms> | device_os, push_token, ttl_heartbeat (10m TTL) |
CONV#<conv_id> | MSG#<timestamp_ms>#<msg_id> | SENDER#<user_id> | MSG#<timestamp_ms> | encrypted_payload, status, ratchet_ephemeral_key |
MAILBOX#<user_id> | MSG#<timestamp_ms>#<msg_id> | CONV#<conv_id> | UNREAD | message_id, sender_id, push_deadline_epoch, escalated_to_high_priority, ttl_purge (90d TTL) |
KEYVAULT#<user_id> | PREKEY#<prekey_id> | DEVICE#<device_id> | KEY#<epoch_ms> | public_identity_key, signed_prekey, signature |
<timestamp_ms> in the message sort key is the router's receive time, never the client's timestamp_ms (device clocks skew by minutes). The conditional put on attribute_not_exists(SK) keyed by msg_id is what makes retransmits idempotent; the router-side timestamp is only for ordering the mailbox scan. Per-conversation total order across group members needs a per-conversation sequence (see the Chat System blueprint in Track 02); the mobile client sorts locally by (timestamp_ms, sequence_number).
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~48%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.