eleven / ocho Public
π€ 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:
- they hold each other's public keys,
- someone generates a thread key and wraps it to both pubkeys (ECIES), and
- 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)
- 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.
- Instance-to-instance federation. Instance A
POSTs 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.
- 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 indocs/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.