Specs
spec/appendix-worked-examples.md · shown exactly as it appears in the repository
Appendix — Worked Examples
License: This text is CC-BY-4.0; the field names, enum values, and other machine-readable schema elements defined here are also MPL-2.0 — see LICENSE-DOCS and LICENSE.
This appendix is non-normative. It traces the message-level mechanics
of 02-connections.md and 03-cross-domain.md against concrete,
illustrative data, so a reader can see the field shapes actually flow
rather than only read the normative rules in isolation. Domain
identifiers, keys, and codes below are placeholders — example.chat is
not a real Bonafide domain.
A. Connection handshake
Two parties, the inviter and the joiner (terminology from
02-connections.md), both run apps built on the example.chat domain.
Step 1 — Inviter generates a key pair and a connection code
The inviter generates an X25519 key pair and a connection code, then registers the code with the router before sharing it out-of-band (e.g. as a QR code):
POST /send-key/register
{
"sendKey": "7bf483tnrpq2",
"receiveUrl": "https://inviter.example.chat/a2a/receive",
"routingToken": "ag3F9...opaque, encrypted with the deployment key...",
"expiryDate": "2026-10-27T00:00:00.000Z",
"domain": "example.chat"
}
Here sendKey is the connection code itself — the router's registry
doesn't distinguish a "code" from an ordinary key, it's simply the value
registered first. The inviter's local connection record is saved with
cxnStatus: "codeSent".
Step 2 — Joiner checks liveness, generates its own key pair
The joiner checks the code is live:
GET /send-key/7bf483tnrpq2
→ { "isCxnCode": true }
It then generates its own X25519 key pair and registers a freshly generated key with the router — the key the inviter will use to reach it from now on:
POST /send-key/register
{
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"receiveUrl": "https://joiner.example.chat/a2a/receive",
"routingToken": "k3nQ1...opaque, encrypted with the deployment key...",
"expiryDate": null,
"domain": "example.chat"
}
The joiner's local connection record is saved with
cxnStatus: "cxnSent".
Step 3 — Joiner sends cxnEstablished
The joiner shapes a message — shapeMessage produces an
envelope/payload pair per 01-messaging.md:
{
"envelope": {
"version": 1,
"messageId": "f3a1c2e4-...",
"sendKey": "7bf483tnrpq2",
"domain": "example.chat"
},
"payload": {
"type": "connection",
"operation": "cxnEstablished",
"messageContent": [{
"sendKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"receiveKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"cxnCode": "7bf483tnrpq2",
"myCxnName": "Sam",
"alias": {"cxnAliasCode": "..."},
"dhPublicKey": "k9f2VqT3...joiner's X25519 public key, base64..."
}]
}
}
Note the envelope's sendKey is still the connection code — this
message is addressed to whatever row the router has for it, which at
this point is still the inviter's codeSent registration. Per
07-security.md, cxnEstablished is encrypted with the bootstrap
deployment key, not a DH-derived secret — none exists yet. Only
payload is encrypted (into encryptedPayload); what actually crosses
the wire to the router is:
POST /a2a-messaging/send
{
"version": 1,
"messageId": "f3a1c2e4-...",
"sendKey": "7bf483tnrpq2",
"domain": "example.chat",
"encryptedPayload": "U2FsdGVkX1...opaque ciphertext..."
}
The router looks up (sendKey, domain), finds the inviter's row, and
forwards — appending the stored routingToken, which it never
decrypts or inspects:
POST https://inviter.example.chat/a2a/receive
{
"version": 1,
"messageId": "f3a1c2e4-...",
"sendKey": "7bf483tnrpq2",
"domain": "example.chat",
"encryptedPayload": "U2FsdGVkX1...",
"routingToken": "ag3F9..."
}
Step 4 — Inviter replaces its registration and computes the secret
The inviter decrypts the payload (with the deployment key), then atomically repoints its router registration from the connection code to the key the joiner generated on its behalf:
POST /send-key/replace
{
"oldSendKey": "7bf483tnrpq2",
"newSendKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"domain": "example.chat"
}
It computes dhSharedSecret from its own private key and the joiner's
dhPublicKey, discards its private key, and updates its connection
record — note the key swap: the joiner's receiveKey (from step 3)
becomes the inviter's own sendKey (how it addresses the joiner from
now on), and the joiner's sendKey becomes the inviter's receiveKey:
{
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"receiveKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"cxnStatus": "cxnEstablished",
"theirDhPublicKey": "k9f2VqT3...",
"dhSharedSecret": "n4Qp8r...base64, never transmitted, computed locally...",
"dhPrivateKey": null
}
Step 5 — Inviter sends complete
{
"envelope": {
"version": 1,
"messageId": "a7c9d1f2-...",
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"domain": "example.chat"
},
"payload": {
"type": "connection",
"operation": "complete",
"messageContent": [{
"myCxnName": "Jordan",
"isA": "connection",
"dhPublicKey": "vR7mZx...inviter's X25519 public key, base64..."
}]
}
}
Still encrypted with the bootstrap deployment key, for the same reason
as step 3 — complete carries the very public key the joiner needs to
compute the shared secret for the first time.
Step 6 — Joiner completes, sends refreshSubscriptions
The joiner computes the same dhSharedSecret from its own private key
and the inviter's dhPublicKey, discards its private key, sets
cxnStatus: "cxnEstablished", and sends subscription /
refreshSubscriptions. From this message onward, every message on this
connection is encrypted with dhSharedSecret rather than the bootstrap
key. The inviter replies with its own alias on this same secret,
completing the one-way-then-the-other alias exchange described in step
3 above.
B. Extending a connection to a new domain, then subscribing
Continuing directly from Example A: Jordan and Sam's connection is
established on example.chat. Jordan also runs an app on a second
domain, example.photos, and wants to extend the same connection there
rather than establish a separate one — per 03-cross-domain.md's
requirement to reuse existing key material instead of generating a new
key pair.
Step 1 — Jordan (requester) sends cxnDomainRequest
Jordan adds the new domain to her own connection record's domain array
and saves a pending cxnDomainInfo entry for it, then sends the
request — addressed under the domain they already share, since
example.photos isn't registered with the router on either side yet:
{
"envelope": {
"version": 1,
"messageId": "b2e4f6a8-...",
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"domain": "example.chat"
},
"payload": {
"type": "connection",
"operation": "cxnDomainRequest",
"messageContent": [{
"isA": "cxnDomainInvite",
"newDomain": "example.photos",
"myCxnName": "Jordan"
}]
}
}
Only after sending does Jordan register her existing receiveKey under
the new domain:
POST /send-key/register
{
"sendKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"receiveUrl": "https://inviter.example.chat/a2a/receive",
"routingToken": "ag3F9...",
"expiryDate": null,
"domain": "example.photos"
}
Per 03-cross-domain.md's register-before-addressed invariant, this
ordering is safe precisely because nothing is routed under
example.photos until Sam replies — the rule doesn't bind this
registration to the send above, since that send was routed under
example.chat.
Step 2 — Sam receives the request
Sam's connection record gets the same domain array update and a
pending cxnDomainInfo entry on her side, with theirCxnName: "Jordan". No message is sent yet — accepting is a separate, explicit
step, same as establishing the connection itself in the first place
(03-cross-domain.md's MUST NOT auto-accept/auto-decline).
Step 3 — Sam accepts: registers first, then sends cxnDomainAccepted
This time the register-before-addressed ordering is load-bearing, since
cxnDomainAccepted itself is routed under example.photos:
POST /send-key/register
{
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"receiveUrl": "https://joiner.example.chat/a2a/receive",
"routingToken": "k3nQ1...",
"expiryDate": null,
"domain": "example.photos"
}
Then:
{
"envelope": {
"version": 1,
"messageId": "c5a7b9d1-...",
"sendKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"domain": "example.photos"
},
"payload": {
"type": "connection",
"operation": "cxnDomainAccepted",
"messageContent": [{
"isA": "cxnDomainInvite",
"domain": "example.photos",
"myCxnName": "Sam"
}]
}
}
The router looks up ("01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44", "example.photos") — the row Jordan just registered in step 1 — and
forwards to Jordan.
Step 4 — Jordan receives acceptance
Jordan marks her own cxnDomainInfo for example.photos as
cxnEstablished, with theirCxnName: "Sam". No further router call is
needed — she already registered under the new domain in step 1. Both
parties' connection records now list domain: ["example.chat", "example.photos"], reusing the same sendKey/receiveKey/
dhSharedSecret as the original connection — nothing new was generated
for the extension.
Step 5 — Sharing content and subscribing on the new domain
With the domain extended, example.photos's own content model applies
(04-access-control.md, 05-content-model.md). Say example.photos
declares album as a subbable content type — per the subbable rule, its
container subscription label is its own ck; a separate, finer-grained
content subscription label, ${albumCK}Content, covers the individual
photos inside it. Extending the connection (steps 1-4 above) only made
example.photos addressable between Jordan and Sam — it granted no
access to anything. The following is the separate, explicit
access-control exchange that actually shares something.
5a. Jordan shares one of her albums with Sam. This adds a local
from subscription (container label = the album's own ck) and sends
the album item itself as ordinary shared content — no subscribe
message is involved on Jordan's side:
{
"envelope": {
"version": 1,
"messageId": "d8f1a3c5-...",
"sendKey": "01933b3e-9a44-7f12-bd03-1c2d3e4f5061",
"domain": "example.photos"
},
"payload": {
"type": "content",
"operation": "create",
"messageContent": [{
"isA": "album",
"ck": "01933c7a-5e22-7d94-8b11-2f3a4b5c6d7e",
"version": 1,
"title": "Summer trip",
"originDOS": 1,
"remainingDOS": 1,
"shareSequence": 42
}]
}
}
5b. Sam receives and persists the album, recorded with
receivedFromCxnId set to this connection (05-content-model.md).
5c. Sam subscribes to the album's content. Having seen the album,
Sam decides (a domain policy choice, not a protocol requirement) to also
receive whatever photos are in it. This adds her own local to
subscription — this time for the content label,
${albumCK}Content, not the container label from step 5a — and sends
the subscribe message that announces it:
{
"envelope": {
"version": 1,
"messageId": "e1a2b3c4-...",
"sendKey": "01933b3e-2f3d-7c91-9a21-8e1a9b7c0d44",
"domain": "example.photos"
},
"payload": {
"type": "subscription",
"operation": "subscribe",
"messageContent": [{
"subLabel": "01933c7a-5e22-7d94-8b11-2f3a4b5c6d7eContent",
"domain": "example.photos",
"subStatus": "subscribed"
}]
}
}
5d. Jordan receives the subscribe message. This adds the mirrored
from/to pair on her own side for the same content label, and
fulfills it — if the album already has photos in it, each one is sent
to Sam the same way the album itself was in step 5a (content /
create, same shareSequence stamped across the whole batch per
06-propagation.md). If the album is still empty, there's nothing to
fulfill yet, and nothing is sent until Jordan adds a photo later.