Realtime architecture
The Realtime implementation consists of three main components:- Server-side Realtime instance - Defines event schemas and handles server-to-client broadcasting
- WebSocket endpoint - Exposes a GET handler for WebSocket connections
- Client-side useRealtime hook - Subscribes to channels and receives events
Server-side configuration
Realtime instance with schema
The Realtime instance is initialized with a Zod schema that defines all possible events:src/lib/realtime.ts
The schema defines two event types under the
chat namespace: message for new messages and destroy for room destruction.Event schema structure
- chat.message
- chat.destroy
Fired when a user sends a message to the room.
WebSocket endpoint
The WebSocket connection is exposed via a Next.js API route:src/app/api/realtime/route.ts
/api/realtime that clients connect to. The handle function:
- Upgrades HTTP requests to WebSocket connections
- Manages subscriptions to channels
- Handles connection lifecycle (connect, disconnect, reconnect)
- Validates event payloads against the Zod schema
Client-side implementation
Creating the useRealtime hook
The client-side hook is generated using thecreateRealtime factory:
src/lib/realtime-client.ts
- Automatically connects to
/api/realtimeWebSocket endpoint - Infers event types from the server schema
- Provides TypeScript autocomplete for event names and payloads
- Handles reconnection logic automatically
The hook must be exported from a
"use client" file since it uses React hooks and browser APIs.Using the hook in components
The room page usesuseRealtime to listen for messages and room destruction:
src/app/room/[roomId]/page.tsx
Hook parameters
channels
channels
Array of channel names to subscribe to. In Private Chat, each room ID is a channel:Multiple channels can be subscribed to simultaneously:
events
events
Array of event names to listen for. Events follow the The hook only receives events that match these names on the subscribed channels.
namespace.eventName pattern:onData
onData
Callback function invoked when an event is received. Provides the event name and payload:
Emitting events from the server
The server emits events using the Realtime instance:src/app/api/[[...slugs]]/route.ts
- Client calls
POST /api/messageswith message data - Server validates auth and saves message to Redis
- Server emits
chat.messageevent to the room’s channel - Upstash Realtime publishes event to Redis pub/sub
- All connected clients on that channel receive the event
- Clients trigger
onDatacallback and refetch messages
Type safety
One of the key benefits of this architecture is end-to-end type safety:If you emit an event that doesn’t match the schema, or try to listen for a non-existent event, TypeScript will catch the error at compile time.
Connection lifecycle
TheuseRealtime hook automatically manages the WebSocket connection:
Initial connection
- When the component mounts, the hook connects to
/api/realtime - Subscribes to the specified channels
- Begins receiving events immediately
Reconnection
- If the connection is lost (network issues, server restart), the hook automatically reconnects
- Subscriptions are restored transparently
- No manual reconnection logic needed
Cleanup
- When the component unmounts, the hook disconnects from the WebSocket
- Subscriptions are cleaned up server-side
- No memory leaks or dangling connections
Message delivery guarantees
Upstash Realtime uses Redis pub/sub under the hood, which provides at-most-once delivery.
- Messages are not persisted by the pub/sub system itself
- If a client is disconnected when an event is emitted, they won’t receive it
- When they reconnect, they must refetch the message history from Redis
- Storing messages in a Redis list (
messages:{roomId}) - Emitting a real-time event to notify connected clients
- Having clients refetch the full message history on every event