@munchi_oy/display-protocol
v0.1.4
Published
Shared wire contract for the Munchi Customer Display System (CDS): pairing, session identity, and cart snapshot messages exchanged over Ably.
Readme
@munchi_oy/display-protocol
Shared wire contract for the Munchi Customer Display System (CDS) — the customer-facing screen that mirrors a POS cart in real time over Ably.
Consumed by three sides:
- POS (
mun-pos) — publishes cart snapshots. - Display app (
gomunchi/CDS) — renders snapshots. - Backend (
munchi-super-server) — mints pairing + session Ably tokens.
This package is types + a few pure helpers only — no runtime dependencies. It exists so a shape change is a compile error in every consumer instead of a silent runtime break. The protocolVersion on every message lets a stale display refuse to mis-render.
Architecture: session/device-based
One business can run multiple POS machines and multiple displays. Pairing is never business-scoped — each pairing links one POS device to one display device via its own session, which owns a dedicated Ably channel.
Display device (displayId) ──┐
├── pairing ──▶ session (sessionId) ──▶ cds:session:{sessionId}
POS device (posDeviceId) ───┘Pairing (code-based, dead-simple first)
- Display requests an Ably token, subscribes to the backend-returned pairing channel (
cds:pair:{displayId}.{env}), and shows a short code (and QR carrying{displayId, nonce}). - POS enters the code (or scans the QR) →
POST /display/pair. - Backend validates, creates the session, returns
{ sessionId, sessionChannel }, and notifies the display on its pairing channel. - Both devices get session-scoped tokens and join the returned session channel (
cds:session:{sessionId}.{env}).
Messages
Envelope: { type, protocolVersion, seq, sentAt, payload } where type is paired | state | unpaired | heartbeat. state payload is a DisplayState discriminated on mode:
| mode | Left panel |
| --- | --- |
| welcome | Munchi logo |
| cart | live line items |
| split_item | per-guest groups |
| done | nothing → welcome |
Snapshots are slim (render-only fields, money in minor units, photos as URLs). Use isWithinSnapshotBudget() before publishing and isNewerSeq() to drop stale/out-of-order messages.
Channels
Channels are environment-scoped (cds:pair:{displayId}.{env}, cds:session:{sessionId}.{env}, matching the platform's existing .${environment} channel convention) because a single Ably app is shared across dev/staging/prod. They are server-authoritative: the backend returns pairingChannel / sessionChannel in its pairing responses and mints the token capability against them, so clients should use the returned strings rather than rebuild names.
Helpers
pairingChannelName(displayId, environment)/sessionChannelName(sessionId, environment)isNewerSeq(incoming, lastApplied)isWithinSnapshotBudget(serialized)/utf8ByteLength(value)
