# KEZ Chat & File Share — Design Document **Status:** Pre-implementation planning. No code yet. **Last updated:** 2026-05-24 **Goal:** A Keybase-class chat + file sharing experience built on the KEZ identity stack, with NATS for messaging and Iroh for file transfer. --- ## 1. What this is A real-time chat + file-sharing application with verified identities. - Users get human-friendly handles like `@tudisco@kez.lat`. - The handle is bound to a KEZ primary key (ed25519); the same key authenticates to the chat infrastructure. - Conversations are end-to-end encrypted; the broker is dumb. - Files are visible in the sender's "shared files" list but only downloaded when a recipient actually wants them. No background sync. - Identity is portable: the underlying key + sigchain survives the home server going dark. Handles can be migrated to other servers. This is the Keybase model rebuilt on a decentralized substrate: - **Identity layer** → KEZ (instead of Keybase's central account system) - **Chat layer** → NATS broker with E2E in the client (instead of Keybase Chat servers) - **File layer** → Iroh peer-to-peer with content addressing (instead of KBFS) --- ## 2. Three-layer architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ kez-chat application │ │ (chat UI, file browser, profile views) │ └────┬──────────────┬─────────────────────┬───────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │ KEZ │ │ NATS │ │ Iroh │ │ │ │ │ │ │ │ ↓ who │ │ ↓ chat │ │ ↓ file blobs │ │ ↓ what │ │ ↓ tickets│ │ ↓ on-demand │ │ they │ │ ↓ presence │ ↓ NAT travers. │ │ own │ │ ↓ small │ │ ↓ E2E in QUIC │ │ ↓ where │ │ stuff │ │ │ │ they │ │ │ │ │ │ listen│ │ dumb │ │ │ │ │ │ broker; │ │ │ │ │ │ clients │ │ │ │ │ │ do E2E │ │ │ └─────────┘ └──────────┘ └────────────────┘ │ ▲ ▲ └─────────── sigchain ──────────────────┘ (handle → KEZ primary → endpoints and links to other identities) ``` Each layer does one thing well. Each is replaceable without touching the others. The KEZ sigchain is the bridge that ties them together — it tells a verifier "this user's broker is X, their Iroh nodes are Y₁ and Y₂." --- ## 3. Identity & username model ### 3.1 Handles Handles look like email and Mastodon addresses: ``` @tudisco@kez.lat @chris@kez.lat @alice@chris.com ← custom domain, opted out of default ``` `kez.lat` is the placeholder default home server domain. We'll replace this with the actual production domain once chosen. The application treats whatever's after the `@` as the user's home server — multiple servers can exist, federation is by convention (same model as email). In the UI, when the home server matches the app's default, handles are displayed bare (`@tudisco`). Custom domains always display the full form (`@chris@chris.com`) so users can tell when they're talking to a non-default-server user. ### 3.2 Key generation tied to username When a user creates an account: 1. App generates a **fresh ed25519 keypair** locally. - This is the user's KEZ primary key. - It's also their NATS nkey for the chat broker (same key, same algorithm). 2. App **registers `@username` on the home server's handle registry** - POSTs a signed registration request: `{ "handle": "tudisco", "primary": "ed25519:" }` - The signature proves the user controls the private key. - The registry rejects squatting (first-come-first-served per home server). 3. App **initializes a sigchain** for the new primary - First event: `add_endpoint` advertising the NATS broker the app will use. - Second event: `add_endpoint` advertising the Iroh NodeId the local app is using. 4. App **uploads the sigchain** to a kez-sig-server (optional but recommended; otherwise the chain lives only on the user's device). After this flow the user has a fully working KEZ identity: - `@tudisco@kez.lat` resolves via the handle registry to their primary key. - That key's sigchain advertises their NATS broker and Iroh nodes. - Other users can verify them and reach them. ### 3.3 Why ed25519 (not nostr/secp256k1) for this app Both KEZ primaries work in general, but the chat app **must** use ed25519 because: - **NATS nkeys are ed25519.** Direct alignment: the user's KEZ primary key is their NATS credential. No second auth scheme. - **Iroh node IDs are ed25519.** Same primitive, native fit. - **One key type to manage.** Users with a pre-existing nostr key can still attach it to their KEZ sigchain as a claim (so they're verifiable on nostr too), but the primary that runs the app is ed25519. --- ## 4. The home server (`kez-chat-server`) A single Rust binary that bundles the home-server responsibilities. One process. Self-hostable. Anyone can run their own to be their own home for their own users. ### 4.1 What it does | Responsibility | How | |---|---| | **Handle registry** | `POST /v1/register` to claim `@username`, `GET /v1/u/` to look one up. SQLite-backed. Same shape as `kez-id-server` discussed earlier. | | **Sigchain mirror** (optional) | Mirrors `kez-sig-server` endpoints for users who don't want to publish elsewhere — `POST /v1/sigchains/.../events`, `GET /v1/sigchains/...`. Or proxies through to a separate `kez-sig-server` instance. | | **NATS broker host** | Runs (or co-runs) a NATS server with JetStream enabled for offline message delivery. Configured to use nkey-based auth tied to KEZ primary keys. | | **Iroh pinning node** | Runs an Iroh node that users can opt to push their blobs to, so files are served even when the user's own device is offline. (Optional per user.) | | **WebFinger endpoint** | `/.well-known/webfinger?resource=acct:tudisco@kez.lat` returns user discovery info — interop with fediverse tools. | | **HTTP API for clients** | Thin REST surface for the chat app to register, look up handles, fetch endpoints, manage settings. | ### 4.2 Process model For MVP, the server is a **coordinator + adapter**, not a full reimplementation: ``` ┌───────────────────────────────────────────────────────────┐ │ kez-chat-server process (one Rust binary) │ │ - HTTP API (axum) │ │ - Handle registry (SQLite) │ │ - NATS auth callout (validates nkey signatures) │ │ - Sigchain mirror (axum routes — could reuse │ │ rust-sig-server code) │ └──┬──────────────────────┬────────────────────────────────┘ │ launches/manages │ talks to via API ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ nats-server │ │ iroh-relay │ (optional, for users │ (Go binary) │ │ (Rust) │ who want pinning) │ + JetStream │ │ │ └──────────────┘ └──────────────┘ ``` The Rust server doesn't reimplement NATS or Iroh — it sits beside them. Operator runs the three processes together (Docker compose, systemd unit, or whatever). The chat-server provides the KEZ-aware integration: authenticating NATS connections against the handle registry, serving sigchain endpoints, exposing a clean HTTP API to client apps. ### 4.3 Endpoints (sketch) ``` GET /v1/healthz GET /v1/u/:handle handle → { primary, sigchain_url, endpoints } POST /v1/register claim a handle (signed body) GET /.well-known/webfinger?resource=... # Sigchain mirror (same as kez-sig-server) GET /v1/sigchains/:scheme/:id POST /v1/sigchains/:scheme/:id/events GET /v1/sigchains/:scheme/:id/head # NATS auth callout (called by nats-server, not by users) POST /internal/nats/auth verify nkey signature, return permissions # Iroh pinning (optional) POST /v1/pin pin a blob for offline serving GET /v1/pin/:hash check pinning status ``` The NATS broker and Iroh node are *out-of-process* — clients connect to them directly (`mqtt://nats.kez.lat:4222`, Iroh direct or via relays). --- ## 5. End-to-end flows ### 5.1 Account creation — `@tudisco@kez.lat` ``` 1. User opens kez-chat-app, clicks "Create account" 2. App: generates ed25519 keypair locally 3. App: user picks handle "tudisco" 4. App → kez-chat-server: POST /v1/register { "handle": "tudisco", "primary": "ed25519:", "registration_sig": "" } 5. Server: validates signature, checks handle is free, stores in registry 6. Server: 201 Created 7. App: initializes sigchain locally, signs: { op: "add_endpoint", payload: { protocol: "nats", url: "nats://nats.kez.lat:4222", inbox: "kez.inbox." } } { op: "add_endpoint", payload: { protocol: "iroh", node_id: "" } } 8. App → server: POST /v1/sigchains/ed25519//events (twice, one per event) 9. App: connects to NATS broker with nkey auth, subscribes to inbox topic 10. Done — user is @tudisco@kez.lat, online, reachable ``` ### 5.2 Adding a contact ``` 1. Tudisco wants to add Chris. Types "@chris" in app. 2. App → kez-chat-server: GET /v1/u/chris Returns: { primary: "ed25519:abc...", sigchain_url: "..." } 3. App fetches the sigchain → walks events → extracts: - nostr/github/dns/etc. claims (for verification) - NATS broker URL + inbox topic - Iroh node IDs 4. App displays Chris's profile: verified accounts, avatar (from sigchain metadata if present), join date 5. App stores LOCAL binding: { "@chris@kez.lat" => ed25519:abc... } (TOFU — trust on first use) ``` ### 5.3 Sending a chat message ``` 1. Tudisco types "hello" in the chat with Chris. 2. App: looks up Chris's primary key + NATS endpoint from local store. 3. App: derives a symmetric key via ECDH: X25519(tudisco_priv, chris_pub) → KDF → 32-byte symmetric key 4. App: encrypts "hello" with ChaCha20-Poly1305 + the derived key. 5. App: signs the ciphertext with tudisco's KEZ primary (so chris can verify the sender, not just decrypt). 6. App: publishes to NATS subject `kez.inbox.` on chris's broker, with JetStream delivery (durable, will queue if chris is offline). 7. Chris's app receives from his subscribed inbox subject. 8. Chris's app: verifies signature against tudisco's key, decrypts, shows "tudisco: hello". ``` For 1:1 chat, the broker never sees: - The message contents - Who tudisco is talking to (the subject is chris's inbox, but anyone could publish there) - The relationship between sender and recipient (sender's identity is in the encrypted+signed payload, not in the NATS metadata) ### 5.4 Sharing a file ``` 1. Tudisco drags `report.pdf` into the chat with Chris. 2. App: imports blob into local Iroh node → gets BLAKE3 hash + ticket. 3. App: optionally adds entry to tudisco's shared-files manifest (visible in his profile if Chris later browses it). 4. App: encrypts the Iroh ticket (and a content key for the blob, if the file is wrapped with a per-recipient symmetric key) with the same E2E mechanism as chat messages. 5. App: publishes to chris's NATS inbox: { type: "file_share", filename: "report.pdf", ticket: "...", content_key: "..." } 6. Chris's app receives the notification, displays: "tudisco shared report.pdf (1.2 MB)" [Download] 7. Chris clicks Download. 8. App: opens Iroh connection to tudisco's NodeId (from sigchain), pulls the blob via the ticket, decrypts with the content key, verifies BLAKE3 hash. File appears. ``` If tudisco is offline at step 8 and he's opted into pinning, Chris's app fetches from `kez.lat`'s pinning node instead. Same protocol, just a different source. ### 5.5 Browsing someone's files (Keybase-style) ``` 1. Chris opens tudisco's profile. 2. App: resolves @tudisco → primary → sigchain. 3. Sigchain has a `set_shared_files` op with a manifest blob hash. 4. App: fetches the manifest blob (small, fast) via Iroh. 5. App: decrypts entries that are wrapped for chris's key, ignores ones it can't decrypt (those are wrapped for other people). 6. App: renders the visible entries with name, size, share date, thumbnail if present. 7. Chris clicks an entry to download — same as 5.4 step 8. ``` The manifest is **small** (KBs); only blobs Chris actually wants are fetched. No background sync of multi-GB folders. --- ## 6. Project & folder layout ### 6.1 Where this project lives ``` /Kez ├── rust-lib/ ← (proposed) shared Rust libraries │ ├── Cargo.toml workspace │ └── crates/ │ ├── kez-core/ moved from rust/crates/ │ └── kez-channels/ moved from rust/crates/ │ ├── rust/ ← Rust CLI (kez binary) │ └── crates/ │ └── kez-cli/ depends on ../../rust-lib/crates/... │ ├── rust-sig-server/ ← optional sigchain HTTP store │ ├── kez-chat/ ← THIS PROJECT │ ├── document.md (this file) │ ├── Cargo.toml │ ├── src/ │ │ ├── main.rs │ │ ├── handles.rs handle registry │ │ ├── sigchain.rs sigchain mirror (or proxy) │ │ ├── nats_auth.rs NATS auth callout │ │ ├── pin.rs Iroh pinning │ │ └── api.rs HTTP routes │ ├── deploy/ │ │ ├── docker-compose.yml chat-server + nats + iroh │ │ ├── nats.conf │ │ └── systemd/ │ └── tests/ │ ├── nodejs/ ← (unchanged) └── crosstest.sh ← (path updates if rust-lib moves) ``` ### 6.2 The `rust-lib/` proposal — share code, no duplication Right now, kez-core and kez-channels live inside `rust/crates/`. The sig-server and the chat-server both want to use them. With everything in `rust/`, downstream projects have to do: ```toml kez-core = { path = "../rust/crates/kez-core" } ``` …which works but feels off (why does a separate project reach into another project's `crates/`?). **Recommendation:** move the pure libraries out into a top-level `rust-lib/` workspace. The CLI stays in `rust/`. Downstream servers depend on `../rust-lib/crates/kez-core`. Clean structure, no duplication, no confusion about which folder owns the library code. Refactor effort: small but real. - `mv rust/crates/kez-core rust-lib/crates/` - `mv rust/crates/kez-channels rust-lib/crates/` - Create `rust-lib/Cargo.toml` (workspace). - Update `rust/Cargo.toml` to have just kez-cli. - Update path deps in: `rust/crates/kez-cli/Cargo.toml`, `rust-sig-server/Cargo.toml`. - Update `crosstest.sh` if any paths are hardcoded. Suggested order: **do the refactor first, then start kez-chat with clean imports.** Otherwise we'll write `path = "../rust/crates/..."` for the chat-server and have to fix it later anyway. ### 6.3 Dependencies (planned) | Crate | Why | |---|---| | `kez-core` (path) | Identity types, sigchain, claim signing | | `kez-channels` (path) | Verify users' linked accounts when displayed | | `axum` 0.8 | HTTP API | | `tokio` | Async runtime | | `rusqlite` (bundled) | Handle registry | | `async-nats` | NATS client (for the auth callout and maybe utility) | | `iroh` | Iroh node (for pinning) | | `iroh-blobs` | Blob handling | | `serde` / `serde_json` | Standard | | `thiserror` / `anyhow` | Standard | | `tracing` / `tracing-subscriber` | Logging | | `tower-http` | CORS, request tracing | | `clap` | CLI args | ### 6.4 The actual NATS broker We don't write a NATS broker. We **run one** alongside the Rust server: - Use the official `nats-server` Go binary (downloaded from nats.io or built from source). - Configure with JetStream enabled (for offline delivery via durable consumers). - Configure auth callout pointing at the kez-chat-server's internal endpoint, so connection auth defers to the KEZ registry. - Run in the same Docker compose / systemd target as the Rust server. NATS clustering for redundancy is a later concern. ### 6.5 The actual Iroh node We DO embed Iroh in-process — Iroh is a Rust library and works as such. The chat-server runs an `iroh::Node` and offers it as a pinning service for users who opt in. For client apps: they run their own Iroh node locally too. The chat-server's Iroh node is just a peer — albeit one that's always online and willing to hold blobs. --- ## 7. MVP scope What ships in v0: - [ ] kez-chat-server binary - [ ] Handle registry (POST /register, GET /u/:handle) - [ ] Sigchain mirror (proxy or own copy) - [ ] NATS auth callout - [ ] WebFinger endpoint - [ ] HTTP healthz/metrics - [ ] NATS broker config + deployment recipe - [ ] Iroh pinning node embedded (optional per user) - [ ] Docker compose for the whole bundle (server + nats + iroh node) - [ ] Integration tests against a real NATS + Iroh What the **client app** needs to do (separate project? `kez-chat-app/`?): - [ ] Account creation flow (key gen + handle registration) - [ ] Contact lookup + verification - [ ] 1:1 chat (E2E via NATS) - [ ] File send/receive (E2E via Iroh) - [ ] Shared-files manifest browse + fetch - [ ] Profile view (sigchain visualization) For v0, **CLI client is fine** (`kez-chat send @chris "hello"`). UI app comes later. --- ## 8. Out of scope (v0) - Group chat - Forward secrecy (Double Ratchet / MLS) — chat is encrypted but not ratcheting in v0 - Voice / video calls - Multi-device key sync — user has one device with their key for v0 - Account recovery / lost-key flows — protocol's `rotate` op exists but UX for recovery isn't designed yet - Federation across home servers — protocol allows it, but the v0 app may only resolve handles on its configured default server - Channel publishing (gist, DNS, ActivityPub, bluesky) — the kez CLI already has these; not duplicated here. User can run `kez claim ...` separately to add channel proofs to their sigchain. - Avatars / display name — could just use `nostr:npub` metadata or a separate sigchain op; defer the design --- ## 9. Open design questions These need resolving before serious implementation: 1. **Bundle or separate sigchain server?** - kez-chat-server includes its own sigchain mirror (one less moving piece for operators) - …or it depends on a separate kez-sig-server (proper layering) - Lean: bundle for MVP, factor out later if multiple chat servers want to share. 2. **Iroh pinning by default or opt-in?** - Default-on: better UX, more storage cost for the server operator - Opt-in: simpler operator story, worse first-use UX for users whose phones are off - Lean: opt-in for v0; let users push the pin button per-file. Default-on later. 3. **NATS broker: bundled or BYO?** - kez-chat-server can spawn/manage `nats-server` as a child process - …or it can assume operator runs NATS separately and just point at it - Lean: BYO with documented config. We don't reinvent process management. 4. **Manifest format** - Single JSON blob, signed, hash committed via sigchain `set_shared_files` op - …or Iroh Doc (CRDT-synced) - Lean: single signed blob for v0; simpler, no Iroh Docs dep. 5. **Handle uniqueness scope** - Per home server (`tudisco@kez.lat` vs `tudisco@example.com` can be different people) - Globally enforced somehow (not really possible without a central registry) - Lean: per home server. Federation handles global resolution. 6. **What about KEZ's existing `nostr:` channel for messaging?** - It already works for chat-like messages via NIP-44 DMs - NATS is a separate stack — not interoperable - Lean: NATS is the chat substrate for this app. Users who want to send a nostr DM can use a separate nostr client. The KEZ identity is the same; the transport is the user's choice per conversation. Document this in the UI. 7. **Recovery story when you lose your key** - Spec has `rotate` op — old key signs that new key is now primary - But if you lost the old key, you can't sign the rotation - Possible solutions: - User must keep paper-backup of their key (Bitcoin model) - User can pre-sign rotation events to multiple device keys (multi-device redundancy) - Home server holds an offline emergency-recovery key (centralized fallback; opt-in) - Defer detailed design to a later doc. --- ## 10. Risks & honest concerns 1. **NATS auth callout integration depth.** The callout pattern is documented but the chat-server needs to handle it correctly for security. nkey signature verification is straightforward but the integration glue (subject permissions per user, JetStream stream creation) needs care. 2. **Iroh is pre-1.0.** API may shift. Pin a version, plan for a future upgrade pass. The good news: identity stays stable (it's KEZ); only the transport library needs to be migrated. 3. **Multi-device.** The MVP assumes one device per user, one key. Real users have phones + laptops. Multi-device key management is a deep topic — addressed in a follow-up doc. 4. **Spam in handle registration.** First-come-first-served is easy to game. Mitigations: - Proof-of-work on registration? - Email-based gating (introduces centralization)? - Rate-limit by IP, accept the leakage - Defer to v0; revisit if it becomes a problem. 5. **NAT traversal for Iroh.** Iroh handles it via relays, but corporate networks are sometimes hostile. Have a "use server's pinning as relay" fallback documented. 6. **Operational cost.** Running NATS + Iroh + a Rust server isn't free. - NATS scales horizontally, low resource use - Iroh nodes can chew through disk if pinning is enabled liberally - Need a clear "I'm running kez.lat for 1000 users — what does it cost?" answer before community adoption. --- ## 11. The plan, sequenced When we start building: 1. **Refactor: move `kez-core` + `kez-channels` to `rust-lib/`**. Tiny but unblocks everything else from having clean imports. 2. **Build `kez-chat-server` scaffold** (axum + sqlite + tracing). Handle registry + WebFinger first — these are the simplest endpoints and unblock client-side account creation. 3. **Add NATS auth callout.** Spawn `nats-server` separately, configure it to call our `/internal/nats/auth` endpoint. End-to-end: client can register a handle and connect to NATS with their nkey. 4. **Build a minimal `kez-chat` CLI client** that does: - `kez-chat register tudisco` - `kez-chat add @chris` - `kez-chat send @chris "hello"` - `kez-chat listen` No UI yet. Enough to prove the chat flow works end-to-end. 5. **Add Iroh integration** to both server and CLI client. - Server: embedded iroh node for pinning - Client: local iroh node, blob send/receive - CLI: `kez-chat share @chris ./file.pdf`, `kez-chat browse @tudisco` 6. **Shared-files manifest** (sigchain `set_shared_files` op, manifest blob format). 7. **Deployment recipe**: docker-compose, systemd unit, deployment doc. 8. **Then** start the GUI app. Could be Tauri (Rust + web frontend), Iced (pure Rust UI), or whatever the user wants. --- ## 12. One-paragraph summary `kez-chat` is a Keybase-class chat and file-sharing app built on the KEZ identity stack. Users get `@username@kez.lat` handles backed by an ed25519 primary key. The same key authenticates to a NATS broker (chat, presence, file tickets — broker is dumb, clients do E2E with ChaCha20-Poly1305 over ECDH-derived keys) and identifies an Iroh node (P2P bulk transfer, content-addressed blobs, on-demand fetch). A single Rust binary (`kez-chat-server`) coordinates the handle registry, NATS auth, optional sigchain mirror, and optional Iroh pinning. The chat-app itself is a separate project that consumes the server's HTTP API plus talks directly to NATS and Iroh.