Architecture
How Verani works under the hood.
Simple Idea
Make realtime on Cloudflare feel like Socket.io.
If you know Socket.io, you already know Verani. The difference: Verani handles Cloudflare Actor hibernation correctly and scales horizontally.
Architecture Options
Verani supports two architectural patterns:
1. Per-Connection Architecture (Recommended)
Each user gets their own Durable Object (ConnectionDO), with separate coordination DOs (RoomDO) for shared state.
Client A ─► ConnectionDO(userA) ─┐
├─► RoomDO("chat") ─► (membership + RPC fanout)
Client B ─► ConnectionDO(userB) ─┤
│
Client C ─► ConnectionDO(userC) ─┴─► RoomDO("presence") ─► (coordination)
Benefits:
- No single-threaded bottleneck
- Horizontal scalability (each user = their own DO)
- Cost-efficient (idle connections hibernate)
- Message delivery via efficient RPC
- Shared state in dedicated coordination DOs
Use createConnectionHandler() and createRoomHandler() for this pattern.
2. Legacy Global Router (Deprecated)
All connections go to a single Durable Object:
Client A ─┐
Client B ─┼─► Single Global DO (handles ALL connections)
Client C ─┘ └─► sessions Map with ALL WebSockets
Problems:
- Single-threaded bottleneck
- O(n) broadcast operations
- Memory pressure from all connections in one DO
- No horizontal scalability
Use defineRoom() and createActorHandler() for this pattern (not recommended for new projects).
Per-Connection Flow
Connection Setup
1. Client connects to Worker
2. Worker extracts userId from request
3. Worker routes to ConnectionDO.get(userId)
4. ConnectionDO owns the single WebSocket
5. ConnectionDO joins RoomDOs as needed
Message Flow (User-to-User via Room)
Client A sends message
↓
ConnectionDO(A) receives via WebSocket
↓
ConnectionDO(A) calls RoomDO.broadcast(event, data)
↓
RoomDO iterates members
↓
RoomDO calls ConnectionDO(B).deliverMessage(event, data) via RPC
↓
ConnectionDO(B) sends to its WebSocket
↓
Client B receives message
Direct Messaging (User-to-User)
Client A sends { type: "dm", toUserId: "B", text: "Hi" }
↓
ConnectionDO(A) receives message
↓
ConnectionDO(A) calls ConnectionDO(B).deliverMessage(...) via RPC
↓
ConnectionDO(B) sends to its WebSocket
↓
Client B receives DM
Durable Object Topology
ConnectionDO (One Per User)
- Identity:
ConnectionDO.get(userId) - Owns:
- Single WebSocket connection (hibernatable)
- Minimal session metadata
- Room membership (persisted)
- RPC Methods:
deliverMessage(event, data)- receive from other DOsjoinRoom(roomName)/leaveRoom(roomName)
RoomDO (One Per Room/Channel)
- Identity:
RoomDO.get(roomName) - Owns:
- Member list (Set of userIds)
- Shared room state
- No WebSocket connections
- RPC Methods:
join(userId)/leave(userId)broadcast(event, data)- fan out to members
Error Handling
- User hooks wrapped in try-catch
- Errors logged with
[Verani]prefix - Optional
onErrorhook for custom handling - Client never sees server errors (security)
- Automatic recovery when possible
Design Principles
- Per-user isolation - Each user has their own DO
- RPC for coordination - DOs communicate via RPC, not shared state
- Hibernation-friendly - State automatically persisted and restored
- No global bottleneck - Horizontal scaling by design
- JSON only - Binary protocols are future work
When to Use Verani
Use Verani when:
- You're on Cloudflare Workers/Pages
- You want Socket.io-like simplicity
- You need automatic hibernation handling
- You need horizontal scalability
Consider alternatives when:
- You need guaranteed ordering (use queues)
- You're not on Cloudflare (use Socket.io, Ably, etc.)