Getting Started
Data Flow & Architecture
Complete data flow diagram showing how events move through every path in the platform, including queues, workers, and independently scalable components.
This page provides a single unified view of every data path in the Realtime Platform — from event origination through delivery to clients and external systems — along with which components can be scaled independently.
Full System Diagram
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ DATA SOURCES │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Application DB │ │ Client SDK / │ │ Client SDK / │ │
│ │ (PostgreSQL) │ │ REST API │ │ REST API │ │
│ │ │ │ sync.create() │ │ socket.publish()│ │
│ │ INSERT/UPDATE/ │ │ sync.merge() │ │ broadcast() │ │
│ │ DELETE on tables │ │ sync.replace() │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ WAL Stream HTTP / WS HTTP / WS │
└───────────┼───────────────────────┼───────────────────────┼──────────────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌────────────────────┐ ┌────────────────────────┐
│ CDC Pipeline │ │ Sync Service │ │ Socket Gateway │
│ ┌──────────────┐ │ │ (HybridDocument │ │ ┌──────────────────┐ │
│ │ WAL CDC │ │ │ Service) │ │ │ Socket.IO Server │ │
│ │ Reader │ │ │ │ │ │ (port 3000) │ │
│ │ │ │ │ Redis hot cache + │ │ └────────┬─────────┘ │
│ │ pgoutput │ │ │ Postgres durable │ │ ┌────────┴─────────┐ │
│ │ protocol │ │ │ with revision │ │ │ WebSocket Server │ │
│ └──────┬───────┘ │ │ tracking │ │ │ (path: /ws) │ │
│ │ │ └─────────┬──────────┘ │ └────────┬─────────┘ │
│ ▼ │ │ │ │ │
│ ┌──────────────┐ │ │ │ │ │
│ │ Mapping │ │ │ │ │ │
│ │ Evaluator │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ • match table│ │ │ │ │ │
│ │ • check ops │ │ │ │ │ │
│ │ • apply when │ │ │ │ │ │
│ │ • build │ │ │ │ │ │
│ │ payload │ │ │ │ │ │
│ └──────┬───────┘ │ │ │ │ │
│ │ │ │ │ │ │
└─────────┼─────────┘ │ └───────────┼────────────┘
│ │ │
│ RawEventInput │ RawEventInput │ Channel broadcast
│ {topic,source, │ {topic,source, │ via ChannelService
│ type,payload} │ type,payload} │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│ EVENT ROUTER │
│ (@realtime/event-router) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ EventNormalizer │──▶│ EventPublisher │──▶│ SubscriptionRegistry │ │
│ │ │ │ │ │ │ │
│ │ Assigns evt_ ID │ │ Publishes to │ │ In-memory Map<topic, Sub[]> │ │
│ │ Wraps in │ │ Redis Pub/Sub: │ │ │ │
│ │ RealtimeEvent │ │ realtime:event: │ │ • Looks up subs by topic │ │
│ │ envelope │ │ {topic} │ │ • Applies EventFilter │ │
│ │ │ │ │ │ (eq, in operators) │ │
│ └─────────────────┘ └─────────────────┘ │ • Calls sub.handler() │ │
│ │ for each match │ │
│ └──────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────────────┘
│
Delivered to sub.handler()
(per-client callback)
│
┌──────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌────────────────┐ ┌──────────────────────┐
│ Socket.IO Client │ │ Plain WS Client│ │ Side Effects │
│ │ │ │ │ │
│ socket.emit( │ │ ws.send(JSON) │ │ See "Side Effect │
│ 'event', data) │ │ │ │ Pipelines" below │
└──────────────────┘ └────────────────┘ └──────────────────────┘Three Event Source Paths
Every event in the platform originates from exactly one of three sources. Each follows a distinct ingestion path before converging at the Event Router.
Path 1: Database CDC
Application DB (external PostgreSQL)
│
│ WAL logical replication (pgoutput protocol)
│ Replication slot: realtime_cdc_slot
│ Publication: realtime_cdc_pub
│
▼
WalCdcReader (one per unique target DB URL)
│
│ Parses XLogData messages → extracts table, operation, old/new row
│ Stores raw CdcEvent in cdc_events table (Postgres)
│
▼
Mapping Evaluator (inside WalCdcReader.processChange)
│
│ For each active CdcSubscription on this table:
│ 1. Load DomainMapping from MappingService
│ 2. Check if operation matches mapping.events[]
│ 3. Evaluate mapping.when conditions (changed, eq, and/or)
│ 4. Build payload by resolving $row.column references
│ 5. Construct RawEventInput { topic, source:'database', type, payload }
│
▼
EventRouter.route(input)
│
▼
Clients receive event on subscribed topicKey details:
- CDC database URL is resolved per-application per-environment from
Application.config.cdc[env] - Each unique target database gets its own
WalCdcReaderinstance - Leader election (Redis SET NX) ensures only one backend instance holds the replication slot
- Reader status is persisted in
cdc_reader_statustable (Postgres)
Path 2: Sync Service
Client SDK / REST API
│
│ POST /api/sync/{service}/documents (create)
│ PUT /api/sync/{service}/documents/{id} (replace)
│ PATCH /api/sync/{service}/documents/{id}/merge (merge)
│ DELETE /api/sync/{service}/documents/{id} (delete)
│
▼
HybridDocumentService
│
│ 1. Check Redis cache first (fast path)
│ 2. If cache miss → load from Postgres → cache in Redis with TTL
│ 3. Apply mutation (replace / deep-merge / delete)
│ 4. Compute diff between old and new data (JsonDiff)
│ 5. Increment revision number
│ 6. Check expectedRevision for optimistic concurrency (409 on conflict)
│ 7. Write to Redis immediately (refresh TTL)
│ 8. Async non-blocking flush to Postgres
│
▼
publishEvent() → EventRouter.route()
│
│ topic: sync.{service}.{operation}
│ source: 'sync'
│ payload: { service, documentId, revision, data, diff }
│ (diff included for update/delete; stripped per subscriber returnMode)
│
▼
Clients receive event on subscribed topicKey details:
- Documents use a hybrid Redis + Postgres architecture: Redis as hot cache, Postgres as durable store
- Active documents stay hot in Redis (default TTL: 10 minutes, configurable via
SYNC_DOC_TTL_SECONDS) - Writes go to Redis immediately → async flush to Postgres (non-blocking)
- Background dirty sweep (every 5s) ensures unflushed docs reach Postgres before TTL expiry
- Falls back to Postgres-only when
REDIS_URLis not set - Revision-based optimistic concurrency control
- Topic naming convention:
sync.{service}.created,sync.{service}.updated,sync.{service}.deleted
Path 3: Socket Service
Client SDK / REST API
│
│ socket.emit('subscribe', { topic }) (Socket.IO)
│ ws.send({ type: 'subscribe', topic }) (plain WS)
│ POST /api/socket/broadcast { channel, event, data } (REST)
│
▼
Socket Gateway (Socket.IO) / WS Gateway (plain WebSocket)
│
│ On subscribe:
│ 1. Generate sub_ prefixed ID
│ 2. Create Subscription { id, clientId, topic, filter, returnMode, handler }
│ 3. Register in SubscriptionRegistry (in-memory)
│ 4. Join Socket.IO room / track in WS channel map
│ 5. Handler wraps applyReturnMode() to strip data/diff per subscriber preference
│
│ On broadcast:
│ 1. ChannelService.broadcast(channel, event, data)
│ 2. Socket.IO: io.to(channel).emit(event, data)
│ 3. Plain WS: iterate wsSenders for channel members
│ 4. Fire broadcastHook → MetricsUpdater → HotTopicsAnalyzer
│
▼
Directly delivered to channel members (no Event Router for raw broadcast)Key details:
- Two parallel gateways coexist on the same HTTP server: Socket.IO (default) and plain WebSocket (
/wspath) - JWT authentication on connection handshake (both gateways)
- When a client subscribes to a topic (not just a channel), the
SubscriptionRegistryhandler routes events from the Event Router directly to that client's socket
Outbox Pattern (Alternative to CDC)
Application code
│
│ BEGIN transaction
│ INSERT INTO orders ...
│ INSERT INTO event_outbox (topic, event_type, payload, table_name, operation)
│ COMMIT
│
▼
OutboxWorker (polling worker, runs in workers app)
│
│ SELECT * FROM event_outbox WHERE processed_at IS NULL
│ ORDER BY id ASC LIMIT 100 FOR UPDATE SKIP LOCKED
│
│ For each row:
│ 1. Build RawEventInput from outbox row
│ 2. EventRouter.route(input)
│ 3. UPDATE event_outbox SET processed_at = NOW()
│
▼
EventRouter.route(input) → ClientsKey details:
FOR UPDATE SKIP LOCKEDenables multiple outbox workers to process concurrently without conflicts- Immediate re-poll when batch returns results (zero-delay tight loop), otherwise polls at configurable interval (default 1s)
- Transactional guarantee: the outbox row and the business write are in the same transaction
Side Effect Pipelines
After events flow through the Event Router to clients, several side-effect systems process events asynchronously.
Webhook Delivery
Gateway event (connect, disconnect, subscribe, broadcast)
│
▼
WebhookDispatcher.dispatch(event) ← fire-and-forget (non-blocking)
│
│ 1. Query WebhookEndpointStore for active endpoints
│ matching event type (or wildcard '*')
│ 2. Filter by applicationId + environment scope
│ 3. For each matching endpoint:
│ a. JSON.stringify(event)
│ b. HMAC-SHA256 signature: t={timestamp},v1={hash}
│ c. POST to endpoint.url with retry (up to 3 attempts)
│ d. Exponential backoff between retries
│ 4. Log result to webhook_logs table (Postgres)
│ 5. Fire onSuccess/onFailure → MetricsUpdater
│
▼
External HTTP endpoint receives signed webhook payloadWebhook Delivery via BullMQ (Workers App)
Event enqueued to 'webhook-delivery' queue (BullMQ / Redis)
│
▼
WebhookDeliveryWorker (in workers app)
│
│ 1. Dequeue job from BullMQ
│ 2. Look up endpoint from WebhookRegistry
│ 3. Sign payload with HMAC-SHA256
│ 4. POST to endpoint URL with timeout + retry
│ 5. Exponential backoff: min(1000 * 2^attempt + jitter, 60s)
│ 6. Failed after maxAttempts → Dead Letter Queue (in-memory)
│
▼
External HTTP endpointAnalytics Pipeline
Events from any source
│
▼
AnalyticsWorker (in workers app)
│
│ ingest(event) → buffer + rolling aggregations
│
│ Periodic flush (default 60s):
│ • Drain buffer
│ • Emit per-topic 1-minute aggregations
│ • { topic, window:'1m', count, startTime, endTime }
│
▼
Analytics storage / downstream consumersAlert Pipeline
MetricsUpdater (5s tick in backend)
│
│ checkAlerts():
│ • webhookFailureRate > 50% → critical alert
│ • webhookFailureRate > 20% → warning alert
│ • activeConnections > 10,000 → warning alert
│
▼
AlertStore (in-memory, ring buffer of 1000)
│
▼
GET /api/alerts → Admin UI Alert CenterMetrics Pipeline
Every gateway event, broadcast, webhook result
│
▼
MetricsUpdater (5s tick)
│
│ Reads live state from:
│ • ChannelService → connection count, channel list
│ • SubscriptionRegistry → subscription count
│ • CdcService → events received/routed, replication lag
│ • WebhookDispatcher callbacks → success/failure counts
│
│ Computes rates and pushes into:
│ • DashboardMetrics → delivery, reliability, pipeline metrics
│ • HotTopicsAnalyzer → per-topic event count, fanout, latency
│
▼
GET /api/metrics/dashboard → Admin UI Dashboard
GET /api/metrics/hot-topics → Admin UI Hot Topics
/metrics → Prometheus scrape endpointRedis Pub/Sub Cross-Instance Fanout
When running multiple backend instances, Redis Pub/Sub ensures events reach all connected clients regardless of which instance they're connected to:
Instance A Redis Instance B
┌──────────┐ ┌────────────┐ ┌──────────┐
│ CDC event │──EventPublisher──▶│ PUBLISH │ │ │
│ on topic │ │ realtime: │──SUBSCRIBE────────▶│ Receives │
│ session. │ │ event: │ │ event, │
│ status │ │ session. │ │ delivers │
│ │ │ status │ │ to local │
│ │ │ │ │ subs │
└──────────┘ └────────────┘ └──────────┘Channel pattern: realtime:event:{topic} (e.g. realtime:event:session.status)
Data Stores
| Store | Technology | Purpose | Scaled By |
|---|---|---|---|
| Platform DB | PostgreSQL | Metadata: topics, schemas, mappings, users, sessions, CDC status, webhook logs, durable sync document storage, deployments | Vertical scaling, read replicas |
| Target DB(s) | PostgreSQL | Application databases monitored by CDC (external, one per app per env) | Independent of platform |
| Redis | Redis + RedisJSON | Hot sync document cache (TTL-based), Pub/Sub fanout, leader election locks, BullMQ job queues, presence | Redis Cluster for horizontal scaling |
Independently Scalable Components
The platform is designed so each component can be scaled independently based on load characteristics:
┌─────────────────────────────────────────────────────────────────────┐
│ SCALABILITY MAP │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Backend Instances (N) │ │
│ │ │ │
│ │ ✅ REST API — stateless, scale out │ │
│ │ ✅ Socket Gateway — stateless with Redis │ │
│ │ ✅ WS Gateway — stateless with Redis │ │
│ │ ⚠️ CDC Reader — ONE leader instance │ │
│ │ ✅ Webhook Dispatch — async, non-blocking │ │
│ │ ✅ Metrics Updater — per-instance local │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Worker Instances (M) │ │
│ │ │ │
│ │ ✅ Outbox Worker — scale with SKIP LOCKED│ │
│ │ ✅ Webhook Worker — scale via BullMQ │ │
│ │ ✅ Analytics Worker — independent flush │ │
│ │ ✅ Alert Worker — per-instance rules │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Infrastructure │ │
│ │ │ │
│ │ ✅ PostgreSQL — read replicas │ │
│ │ ✅ Redis — Redis Cluster │ │
│ │ ✅ Admin UI — static SPA, CDN │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Component Scaling Details
Scale: Horizontally (add more instances behind a load balancer)
The backend is stateless — all persistent state lives in PostgreSQL and Redis. Adding more instances increases:
- HTTP request throughput (REST API)
- WebSocket connection capacity (Socket.IO + plain WS)
- Webhook dispatch parallelism
Constraint: CDC readers use Redis-based leader election. Only one instance holds the replication slot at a time. If the leader dies, another instance acquires the lock within ~5 seconds (heartbeat 3s, follower poll 5s, plus Redis pub/sub signal for immediate failover).
Load Balancer (sticky sessions for WS)
│
┌────┼────┬────────┐
▼ ▼ ▼ ▼
B1 B2 B3 ... Bn
│ │ │ │
└────┴────┴────────┘
│
Redis Pub/Sub
(cross-instance fanout)Event Envelope Reference
Every event flowing through the platform uses this normalized envelope:
interface RealtimeEvent {
id: string; // e.g. evt_a1b2c3d4
topic: string; // e.g. session.status
source: 'database' | 'sync' | 'socket';
type: string; // e.g. database.update
payload: Record<string, unknown>; // sync events: { service, documentId, revision, data?, diff? }
metadata: {
timestamp: number;
table?: string; // database events only
operation?: 'insert' | 'update' | 'delete';
revision?: number; // sync events only
};
}End-to-End Latency Breakdown
Database CDC Path (target: < 150ms end-to-end):
WAL write → pgoutput decode ~10-30ms
Mapping evaluation ~1-5ms
EventRouter normalize+publish ~5-15ms
Redis Pub/Sub fanout ~1-5ms
Socket delivery to client ~5-20ms
Sync Service Path (target: < 50ms):
HTTP request parsing ~1-2ms
Redis cache read (hit) ~1-3ms (or Postgres fallback ~10-25ms)
Redis cache write + TTL ~1-3ms
Async Postgres flush ~0ms (non-blocking, background)
EventRouter normalize+publish ~5-15ms
Socket delivery to client ~5-20ms
Socket Broadcast Path (target: < 100ms):
Message received by gateway ~1-2ms
Channel broadcast via IO ~5-30ms (depends on subscriber count)
Redis cross-instance fanout ~1-5ms (if multi-instance)Complete Wiring Summary
This table shows how every major component is wired together at startup in apps/backend/src/index.ts:
| Component | Created From | Wired To |
|---|---|---|
EventRouter | noopPublisher + subscriptionRegistry | CdcService, HybridDocumentService |
SubscriptionRegistry | standalone (in-memory) | EventRouter, Socket Gateway, WS Gateway, Socket API |
ChannelService | Socket.IO Server | Socket Gateway, WS Gateway, MetricsUpdater |
CdcService | stores + applicationService + mappingService + eventRouter | CDC API, MetricsUpdater |
WalCdcReader | created on-demand by readerFactory | CdcService (per target DB) |
CdcLeaderElection | Redis clients (main + sub) | CdcService (optional) |
WebhookDispatcher | webhookStore + logStore + callbacks | Socket Gateway, WS Gateway |
MetricsUpdater | dashboard + hotTopics + channelService + subscriptionRegistry | Broadcast hook, periodic 5s tick |
HybridDocumentService | RedisJsonClient + Knex db + EventRouter (when REDIS_URL set) | Sync API routes |
PgDocumentService | Knex db (fallback when no REDIS_URL) | Sync API routes |
JwtService | SigningKeyStore (pre-loaded from Postgres) | WS Gateway auth, Auth middleware |