# 🤖 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.
