Building an Applet
The Bridge Protocol
Introduction
Typically, you will never need this page. Use @ziprack/sdk instead: it is the reference implementation of everything below, plus mock mode and a test host.
This page is for when you talk to the Rack without the SDK, for example from a language or runtime the SDK does not cover.
The Envelope
Every message in both directions is one JSON object:
{ "ns": "ziprack", "v": 1, "type": "activity:statement", "requestId": "b1f0…", "payload": { } }| Field | Rules |
|---|---|
ns |
Always ziprack. Anything else is ignored. |
v |
Always 1. Versioned with the format string, not with capabilities. |
type |
rack:* for lifecycle, or {capability}:{action} matching ^[a-z][a-z0-9_]*:[a-z][a-z0-9_.]*$. |
requestId |
Present when you want an ack. The SDK uses crypto.randomUUID(). |
payload |
Type-specific object. |
The applet posts to window.parent with target origin *, because it has no origin of its own to assert.
Accept a message only when its event.source is window.parent, and do not check event.origin. That is what keeps a nested frame, such as an embedded video player, from impersonating the Rack.
Handshake
The applet opens the conversation:
applet → host { type: "rack:ready" }
host → applet { type: "rack:launch", payload: { …launch data… } }The applet announces readiness as early as it can, and the host replies with the full launch payload (see Launch Data). Do not send capability calls before rack:launch arrives. Queue them instead, as the SDK does.
The core of the payload is schema, actor, applet, capabilities, media and roles. roles holds the participant's roles in this applet as a list of names, and is sent whatever the manifest declares. Each declared capability adds its own key.
Calls and Acks
A call carries a requestId, and the host answers with a rack:ack under the same id:
applet → host { type: "db:create", requestId: "r1", payload: { collection: "ideas", data: {…} } }
host → applet { type: "rack:ack", requestId: "r1", payload: { ok: true, data: { record: {…} } } }An ack always carries the requestId it answers. On failure, the payload looks like this:
{ "ok": false, "error": "quota_exceeded", "message": "Collection [ideas] is full (500 records)." }Error codes are listed in Acks and Errors and are part of the contract: they do not change within ziprack/1.
A message sent without a requestId is fire-and-forget. The host processes it and sends nothing back. rack:eject is the usual example.
Streaming
A streamed call receives multiple acks under one requestId:
{ "ok": true, "partial": true, "delta": "Well, " }
{ "ok": true, "partial": true, "delta": "that depends…" }
{ "ok": true, "done": true, "data": { "message_id": "m_1", "text": "Well, that depends…", "usage": {…} } }Only the terminal ack resolves the call, and each partial resets the ack timeout. ai:chat is the only streaming surface in ziprack/1.
Events
Unsolicited messages from the host carry no requestId:
{ "ns": "ziprack", "v": 1, "type": "rack:event",
"payload": { "event": "advance", "payload": { "step": 3 }, "channel": "main",
"from": { "participant_id": 42, "role": "facilitator" } } }The reserved channel $room carries Rack-generated room events: presence, state, role and status. An applet cannot publish to it.
Delivery and Timeouts
Your calls do not necessarily reach the Rack one at a time. Your applet must behave correctly when:
- Acks are delayed. Calls may be queued briefly and delivered together, so an ack is not immediate even on a fast network. Acks still come back in order, one per call.
- State is coalesced. Only the latest pending
attempt:stateneeds to survive, which is what makes callingsetState()on every keystroke safe. - Delivery fails. A failed delivery may be retried. Once the Rack gives up, every message in it acks
persist_failed. - The page is hidden. Pending messages are flushed when the page is hidden, so work survives the participant closing the tab.
storage:put carries a File, which travels to the Rack out of band. The File never travels as JSON.
A successful attempt:complete switches on the Rack's own completion chrome, which is why a completed attempt lands on its final screen rather than restarting. See Completion.
The SDK waits 5 seconds for an ack by default. storage:put allows 120 seconds, because upload time is participant time, and a streamed ai:chat call allows 60 seconds in total.