PATTERN Cited by 1 source
WebSocket relay over logical replication¶
Problem¶
You want to deliver database-sourced events to browser clients in real time. Browsers can't subscribe to a Postgres replication slot directly — there's no way for a browser to speak the Postgres streaming-replication protocol, and you wouldn't want it to even if it could (auth + backpressure + ordering-state-per-session would all be a mess).
Solution¶
Put a small relay process between Postgres and browsers:
- Server-to-database side: the relay opens a long-lived logical replication subscription against a Postgres publication.
- Server-to-browser side: the relay accepts WebSocket
connections from browsers, routes inbound messages into
INSERTs on the publication's tables, and fans out received replication events to whichever connected clients the event is addressed to.
The relay is the narrow waist: browsers speak WebSocket; Postgres speaks logical replication; neither has to know the other exists.
Architecture¶
Browser ──WebSocket──▶ relay ──INSERT──▶ Postgres (publication)
Browser ◀─WebSocket── relay ◀─replication── Postgres (slot)
The write and fan-out paths share the database as the single source
of truth. Per-event routing is trivial — each row carries its own
to_id / session_id / channel_id column, and the relay consults
its in-memory map of {client → websocket} to forward the event.
Mechanism details from the canonical instance¶
From sources/2026-02-27-planetscale-video-conferencing-with-postgres
(Nick Van Wiggeren's pg-relay, ~400 lines of TypeScript):
- Browser captures a JPEG frame, packs it into a binary WebSocket message with a small JSON header (session ID, sequence number, recipient), and sends it to the relay.
- Relay validates the session is active, then:
INSERT INTO video_frames (session_id, from_id, to_id, seq, width, height, jpeg)
VALUES ($1, $2, $3, $4, $5, $6, $7)
- Postgres writes the row to the WAL (which is also the logical- replication stream).
- Relay's logical-replication consumer on the same database sees
the new row, reads the
to_idcolumn, looks up the corresponding WebSocket in its in-memory session table, and forwards the raw JPEG bytes over WebSocket to the recipient's browser. - Recipient's browser creates a blob URL from the JPEG and renders
it in an
<img>element.
Audio flows through the parallel audio_frames table with the same
relay handling PCM samples the same way.
Chat, presence, and call state all flow through the same relay
subscribed to the same publication — the relay is domain-
agnostic once routing is just to_id column lookup + WebSocket
forward.
Why not just a custom pub/sub server?¶
- The database is already durable. Events persisted for replication subscription are also crash-safe and queryable later. Van Wiggeren notes: "Each JPEG is being durably persisted to PostgreSQL, crash-safe, replicatable and ready for querying later on."
- No dual-write or outbox pattern needed. The INSERT by the relay is the publish.
- The relay is stateless-ish. All its state (in-memory
{client → websocket}map) can be rebuilt from the publication + session table on restart. Crash the relay, and it catches up from its last acknowledged LSN in the replication slot.
Operational considerations¶
- Slot liveness = WAL liveness. If the relay disappears, the slot it holds prevents WAL recycling. Add a health-check watchdog that drops the slot if the relay isn't present. (Standard operational concern for any replication-slot consumer — see concepts/postgres-logical-replication-slot and concepts/runaway-wal-growth.)
- Horizontal scale is by adding relays, not slots. Each relay gets its own slot on the same publication; Postgres supports many slots on the same publication independently.
- Backpressure. If the relay's WebSocket fan-out is slower than the publication's insert rate, the relay's slot falls behind and WAL accumulates. Standard streaming-application concern.
- Session affinity matters: a browser talking to relay A needs
to be reachable from relay A's
{client → websocket}map. Most deployments pin browsers to a relay via load-balancer affinity or an external routing layer.
Canonical wiki instance¶
sources/2026-02-27-planetscale-video-conferencing-with-postgres:
pg-relay, Nick Van Wiggeren's ~400-line Node.js TypeScript
WebSocket server, routes bidirectional 15 fps video + audio + chat
+ presence traffic between SvelteKit browsers and a $5 PlanetScale
Postgres. Same binary handles both inbound (browser → INSERT) and
outbound (replication → WebSocket) paths. Source lives at
github.com/nickvanw/PgVideoChat.