Version v3.0.0 Size 1.7 KB gzip
Why Tavern?
Peer-to-peer apps share one arrangement problem: the canonical state of something — a document, a dashboard, a record — lives on exactly one device, while every other participant should see it and be able to change it from their own. Tavern turns that arrangement into a session: one host owns the canonical state, guests mirror it, and every guest command is forwarded through the host's own command table — so remote actions follow exactly the same code path as local ones.
// Before: hand-rolled session orchestration — pairing, protocol validation,
// command routing, snapshot broadcasting, notice relaying, presence tracking.
const host = createMeshHost({ /* ... */ });
host.on('command', ({ peerId, payload }) => {
const command = mySchema.safeParse(payload);
if (!command.success) { host.send(peerId, 'rejected', { /* ... */ }); return; }
try { applySubjectCommand(command.data.name, subject, command.data.args); }
catch (error) { host.send(peerId, 'rejected', { /* ... */ }); }
});
bus.on('subject:updated', () => { host.broadcast('snapshot', snapshotFor(subject)); });
// ... 40+ more lines of plumbing
// After: tavern owns the protocol, the validation, the broadcasting.
const host = hostTavern({
commands: { apply: (name, args) => store.apply(name, args), has: (name) => name in store },
subjectId: 'doc-1',
subjects: {
onChanged: (listener) => store.onUpdated('doc-1', listener),
onRemoved: (listener) => store.onRemoved('doc-1', listener),
snapshot: () => store.read('doc-1'),
},
});| Feature | Tavern | Raw mesh | Socket.io rooms |
|---|---|---|---|
| Bundle size | 1.7 KB | 6.0 KB | — |
| Zero dependencies | |||
| Host-authoritative commands | |||
| Snapshot replication | |||
| Out-of-band pairing (QR / copy-paste) | |||
| Notice relaying |
Use Tavern when one device should own the canonical state of something — a shared document, a live dashboard, a record under review — and every other device should mirror it and send commands back through the host's own command table, which stays the single validation path.
Consider raw @vielzeug/mesh when you need P2P messaging without the host-authoritative replication model — chat, whiteboards, file transfer, or custom topologies.
Installation
pnpm add @vielzeug/tavernnpm install @vielzeug/tavernyarn add @vielzeug/tavernQuick Start
import { hostTavern, joinTavern } from '@vielzeug/tavern';
// Host: pick a subject, wire the command table and snapshot access.
const host = hostTavern({
commands: {
apply: (name, args) => myStore.apply(name, args),
has: (name) => name in myStore,
},
subjectId: 'doc-1',
subjects: {
onChanged: (listener) => myStore.onUpdated('doc-1', listener),
onRemoved: (listener) => myStore.onRemoved('doc-1', listener),
snapshot: () => myStore.read('doc-1'),
},
});
// Produce a single-use invitation; deliver it out-of-band (QR, copy/paste).
const invitation = await host.createInvitationText();
// Guest: consume the invitation, produce an answer for the host.
const { answerText, guest } = await joinTavern({
invitationText: invitation,
mount: (snapshot) => parseAndMount(snapshot),
name: 'Alex',
});
await host.acceptAnswerText(answerText);
// The guest's commands run through the host's own table.
guest.sendCommand('doc-1', 'rename', ['Quarterly report']);
// Cleanup — hosting ends for every guest; onEnded fires exactly once on both sides.
host.dispose();
guest.dispose();Features
hostTavern— host one subject: pairing, protocol validation, command application, coalesced snapshot broadcastsjoinTavern— join as a guest: consume the invitation, produce the answer, mirror the host's snapshotsTavernCommands— the host's command table: the same object the host's own UI calls, so remote actions cannot bypass validationTavernNotices— consumer-defined notice serialization: catalog keys cross the wire, each client translates locally- Coalesced snapshots — bursts of local changes ship one snapshot per microtask, not one per change
onEndedfires exactly once — on both handles: channel drops, subject removal, and explicit disposal share a single idempotent cleanup path- Typed errors —
TavernPairingErrorcovers every unusable pasted code with the mesh failure chained ascause