docs: propose a cross-instance relay protocol for discussion (#153)
Design sketch (no code) exploring whether two people on different, fully-isolated instances could chat. Key framing: because the server is already untrusted, a relay is cryptographically almost free — the real costs are cross-instance metadata (today the cross-circle graph lives nowhere) and losing invite-gating as the anti-abuse mechanism (needs a consent model). Covers three topologies (client-as-relay, federation, central relay), a recommended lightest shape reusing the existing invite key-shipping, and open questions. Filed for future discussion.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+122
−0
docs/cross-instance-relay.md+122 −0
diff --git a/docs/cross-instance-relay.md b/docs/cross-instance-relay.md| new file mode 100644 |
| index 0000000..6dff11a |
| --- /dev/null |
| +++ b/docs/cross-instance-relay.md |
| @@ -0,0 +1,122 @@ |
| +# Cross-instance relay — chatting across the trust boundary |
| + |
| +Status: **proposal / for discussion** (no code). This is a design sketch, not a |
| +commitment. It builds on `docs/threads-and-dms.md` (user keypairs, per-thread |
| +keys, server-blind key delivery) and deliberately re-opens something that |
| +document calls out as *"intentionally impossible"* — cross-instance contact. |
| + |
| +Today each chat is a **fully-isolated instance**: its own Go binary, SQLite DB, |
| +admin token, and subdomain. A user is a member of exactly one roster; a message |
| +is fanned out in-process to WebSocket clients on that same server and can't leave |
| +the box. There is no federation, no server-to-server protocol, no user discovery |
| +across instances. This doc asks: *could* two people on different instances chat, |
| +and if so, what would it actually cost? |
| + |
| +## The key insight: the crypto is the easy part |
| + |
| +E2EE here is about **keys, not servers**. A "user" is an ECDH P-256 public key |
| +with no inherent instance affiliation; a thread key is AES-GCM, wrapped to each |
| +member's public key via ECIES; the server only ever holds blobs it can't open, |
| +and messages are self-describing ciphertext (filename/MIME ride *inside* the |
| +payload — `crypto.js`). |
| + |
| +So for Alice@family to DM Bob@friends, the cryptographic need is only: |
| + |
| +1. they hold each other's **public keys**, |
| +2. someone generates a thread key and **wraps it to both** pubkeys (ECIES), and |
| +3. both can **fetch** the ciphertext + their own wrapped key blob. |
| + |
| +None of that requires them to share a server. A relay would see exactly what |
| +today's server already sees — opaque bytes. **The E2EE invariant survives |
| +untouched**; the server was never trusted with plaintext in the first place. The |
| +wire format needs no real change: `iv||ciphertext` base64url blobs plus one |
| +wrapped-key blob per recipient. |
| + |
| +What a relay actually costs is **metadata privacy** and the **invite-gated trust |
| +boundary** — not confidentiality. Those, not the encryption, are the design work. |
| + |
| +## Three topologies (lightest → heaviest) |
| + |
| +1. **Client-as-relay (no new server).** Alice is a member of both instances; her |
| + browser posts the same ciphertext to both DBs. Keys/discovery bootstrap via an |
| + out-of-band invite — *exactly today's invite-fragment mechanism*. Cheapest, |
| + leaks the least (no new observer), but there's no offline delivery to a peer |
| + who isn't co-homed, and the "same human, two identities" model is awkward. |
| + |
| +2. **Instance-to-instance federation.** Instance A `POST`s ciphertext to an |
| + inbound endpoint on instance B. Needs server-to-server auth, a way for A to |
| + discover B's address, and each instance to trust the other's routing claims. |
| + Distributes metadata (no single chokepoint) but multiplies the number of trust |
| + relationships an admin has to reason about. |
| + |
| +3. **Central relay / mailbox.** A third dumb server both instances push to and |
| + poll, addressing messages by recipient public key (or a hash of it). Best UX — |
| + directory-style discovery and offline delivery — but it concentrates routing |
| + metadata in one place. |
| + |
| +## What you'd actually be trading away |
| + |
| +The encryption doesn't weaken, but three real properties do. These are the |
| +decisions, and they're why this is a *proposal* and not a PR that ships code. |
| + |
| +- **Cross-instance metadata stops being non-existent.** Today, isolated instances |
| + mean the cross-circle social graph *lives nowhere* — no server can observe that |
| + a member of a kid's-friends instance also talks to a work contact. Any relay |
| + creates a place where `{pubkey X ↔ pubkey Y, time T, size N}` is visible, even |
| + with content sealed. This is precisely the property the isolated-instance design |
| + exists to provide (see the metadata note in |
| + `docs/onboarding-and-home-server.md`). Losing it is the central cost. |
| + |
| +- **Invite-gating stops being the anti-abuse mechanism.** Right now "you can only |
| + be reached if an admin put you on the roster" *is* the spam/abuse control. |
| + Cross-instance reachability punches through it, so it must be replaced by an |
| + explicit **contact-request / consent** model: no one can open a cross-instance |
| + thread with you until you accept. |
| + |
| +- **Governance spans two admins.** A shared thread lives under two rosters with |
| + two admins and two removal/rotation policies. Whose rules win? Removing someone |
| + from one instance doesn't evict them from a thread hosted (or mirrored) on the |
| + other; a thread-key rotation on one side has to propagate across the boundary. |
| + Push, too, is per-instance today (APNs topic per instance, subscriptions keyed |
| + by token) — cross-instance delivery needs the peer/relay to trigger it, adding |
| + metadata at the push layer. |
| + |
| +## A recommended shape (if we do this) |
| + |
| +Frame it honestly: **this is a metadata-and-consent project, not a crypto |
| +project.** The lightest design that keeps the most of what makes lchat good is |
| +option 1 stretched slightly: |
| + |
| +- A **cross-instance invite** bootstraps a shared thread, reusing the exact |
| + key-shipping already built for onboarding: the invite fragment carries the |
| + thread key wrapped to the recipient, exchanged out-of-band (so no central |
| + directory is introduced, and discovery stays a human act). |
| +- The thread's ciphertext is **mirrored to both instances** so neither side needs |
| + to trust a new server and either instance alone can serve history. |
| +- A **consent step** gates the first message, replacing roster-gating as the |
| + abuse control. |
| + |
| +Reach for a central relay (option 3) only if offline delivery and directory-style |
| +discovery are worth conceding the metadata point — a genuine trade, not a default. |
| + |
| +## Open questions |
| + |
| +- **Identity across instances.** Is a person's cross-instance identity their |
| + existing per-instance user keypair, or a separate long-term "contact" key they |
| + present to multiple instances? The latter enables real discovery but creates a |
| + correlatable global identifier — the thing isolation currently prevents. |
| +- **Where history lives** when the two instances have different retention/deletion |
| + and one deletes the shared thread. |
| +- **Trust display.** How does the UI make it unmistakable that a thread crosses |
| + the trust boundary (different admin, different rules) rather than looking like |
| + any other DM? |
| +- **Blast radius on compromise.** A relay or federation endpoint that's reachable |
| + is new attack surface; even serving only ciphertext, it becomes a place to |
| + correlate the graph. Worth a threat model of its own before any code. |
| + |
| +## Non-goals (for this doc) |
| + |
| +No implementation, schema, or endpoints are proposed here. Sealed-sender / |
| +private-membership (hiding the graph even from the relay) is a separate, harder |
| +problem and explicitly out of scope. This exists to make the trade-offs |
| +reviewable so a future decision is deliberate, not incremental. |