๐Ÿ”„

Tab Sync Dashboard

postal BroadcastChannel transport demo

synced
Tab: โ€”

Auth

Login syncs across all open tabs

Open another tab and log in โ€” both tabs will update.

Theme

Toggle syncs instantly across all tabs

๐ŸŒ™ Dark mode

Preferences

Changes sync across all tabs and persist on reload

Notification sounds
Play a sound on new notifications
Compact view
Reduce spacing for denser layout

Activity Log

All messages โ€” powered by wiretap

0
๐Ÿ“ก

Interact with the controls
or open another tab

How postal makes this work

Cross-tab state sync with localStorage for persistence, BroadcastChannel for real-time notification, and ServiceWorker MessagePort for presence coordination. Two transports, one postal instance.

BroadcastChannel transport

Two lines of setup: createBroadcastChannelTransport("tab-sync-demo") and addTransport(transport). No handshake, no connection management โ€” any tab that opens the same-named channel is immediately in the mesh.

The production pattern

localStorage holds the authoritative state โ€” a new tab that loads while others are open hydrates from storage via loadState() and renders immediately. Live changes sync via postal so every open tab stays consistent without polling.

Echo prevention in postal core

Outbound envelopes are stamped with source: instanceId. Inbound envelopes matching the local instance are dropped in transport.ts before subscribers fire. User actions just publish โ€” subscribers handle all state mutation regardless of origin.

Wiretap observability

addWiretap(callback) registers a global observer that sees every message on the bus โ€” both local publishes and messages arriving from other tabs via the transport. The activity log above is 100% wiretap.

Dual transports, one instance

The same postal instance runs two transports simultaneously: BroadcastChannel for fast tab-to-tab fan-out (no SW roundtrip) and a MessagePort for point-to-point tab-to-SW communication. The SW is a separate JS context with its own postal instance โ€” connectToServiceWorker() does a MessageChannel handshake, not a broadcast. The "Tabs:" count in the header comes from the SW publishing sync.clients.changed whenever a tab connects or disconnects. Note: port close can be delayed on tab unload in older browsers โ€” production apps should add a heartbeat for reliability.