Kleo roadmap
The plan from PROJECT_BRIEF.md §8, revised with decisions made since. The brief remains the product vision; this file is the current plan.
Principles
- A BitName is the identity. Keys, addresses, profile, inbox and channels all hang
off a human-readable name that is verified on-chain.
- Spam-proof by construction. Only BitName holders can post, publish or be
hosted. Where that isn't enough, reaching someone costs a small payment.
- Infrastructure is content-blind. Resolvers and relays move and serve bytes that
clients verify against the chain. They can withhold data, but they can't read or forge it.
- Not a Telegram clone. No central server, no media pipelines. Chat and channels
run over peer-to-peer links plus replaceable relays.
- Settled decisions:
- Only BitNames' own cryptography; no GPG/OpenPGP. - No Lightning Network. Payments use BitNames sidechain sats (plus Bitcoin and Paymail addresses as published payment methods).
Status
| Phase | What | State |
|---|---|---|
| 1 | Plumbing: node connection, settings, status | Done (392037a) |
| 2 | Notepad: sign / verify / encrypt / decrypt by BitName | Done (392037a) |
| 3 | Identity: wallet seed, registration, key rotation, contacts | Done (392037a) |
| 4 | Public profiles (linktree / phone book), kleo-resolver | Done (282cfdd) |
| 5 | Files: signatures, encryption, checksums | Done (0a4dac5) |
| 6 | Direct messages, peer to peer (iroh) | Done (5af452e) |
| 7 | Offline delivery: kleo-hub mailboxes and endpoint directory | Done |
| 8 | Everyday chat: notifications, tray, blocking, attachments, search, export, second device | Done |
| 9 | The wallet: receive, pay, deposit, withdraw, history | Done |
| 10 | Payments and paid messages: paymail, prices, allow list | Done |
| 11 | Channels: broadcast, private groups, communities | Done |
| 12 | Release engineering: builds, audit, threat model, onboarding | Partly done |
| 13 | The BitNames design: five sections, the directory, the two-pane workbench, the activity log | Done |
| Dropped (BitNames crypto only) |
Last shipped
Phase 13: the BitNames design
The five mockups in bitnames-kleo-spec.md, built. The nav leads with Coins, Lookup, My PayMail, Messaging, Activity and keeps the rest of Kleo below them.
- Lookup lists every registration the chain holds, sortable and searchable, with
the selected name's keys, holding address, away message, links and PayMail fee, plus a right-click menu and an inline composer that quotes the price first.
- Messaging is two panes — plaintext left, cyphertext right, always — with a
direction toggle and a live transform. From, To, Sign message and Password are in the notepad rather than behind a tab. Several recipients get a block each; Password encrypts to nobody at all.
- My PayMail holds what strangers paid to reach you and the single control that
sets the price.
- Activity reads the chain as a log, filterable, every named line opening that
name in Lookup.
- What the chain would not give: registration dates (a BitName is not stamped with
one) and the words behind a hash (only blake3(name) is published). Both are shown as what they are — a sequence id and a hash — rather than guessed at. See §11 of the spec.
- The chain keeps no history, so Kleo keeps one: each reading of the whole name
set is compared with the last, and what differs — a rotated key, a new profile, a changed price or address — is recorded locally with the time it was seen. The first reading is a baseline, and the interface says how far back the record goes.
- Refresh chain in the sidebar takes that reading on demand and reports what it
found; nothing polls the name set, which is the heaviest call Kleo makes.
- Themes are pickable in Settings: follow the system, light, dark, Breeze
(KDE Plasma) and Adwaita (GNOME/GTK4). Each is Kleo's own tokens in another desktop's palette, with light and dark sets so they follow the machine.
- Not yet: a single envelope for several recipients, a hub-backed index of names,
and sampling while Kleo is closed — the record only grows when it is open.
Phase 6: direct messages (online, peer to peer)
1:1 chat between BitNames, over iroh QUIC.
- Addressing is by name: the endpoint is published in the recipient's profile
under chat.endpoint, so it is covered by their on-chain commitment and no server can substitute another one. Kleo publishes your own with one profile update.
- Transport is a direct hole-punched link where one can be made, and otherwise a
public relay that only ever carries ciphertext. That is separate from the store-and-forward relays in phase 7, which hold messages for people who are offline.
- Encryption: every message is signed by the sender's BitName key and encrypted to
the recipient's, through the node, exactly as in the workbench. The transport is never trusted: what the signature says beats what the envelope claims, and a message from a device the sender doesn't publish is shown as such.
- History is stored locally, encrypted in the same vault as contacts.
- Deferred: a key ratchet for forward secrecy, which would keep the BitName keys
only to authenticate the session. Worth doing once relays (phase 7) make sessions long-lived.
- Known limit: both sides must be online at the same time — that is what phase 7
fixes.
Seed nodes and hubs (decided)
Chat today reaches people through [iroh]'s default infrastructure: Number Zero's relay servers, and their pkarr/DNS service at iroh.link, where each app publishes signed records of the addresses it can currently be reached at. That works, but it is the only part of Kleo that depends on somebody else's servers, it isn't configurable, and a BitName publishes only an endpoint id — so if that lookup is unreachable there is no other way to find the machine behind it.
Kleo will ship its own seed nodes, called hubs, and run some of them itself — but a hub is infrastructure, not an authority. Everything it serves is self-certifying:
| What a hub holds | Who signs it | What a hub can do | What it can't |
|---|---|---|---|
| Profile documents | the name's on-chain key, checked against the commitment | withhold, serve a stale copy | alter a field, invent a profile |
| Endpoint records (which addresses a machine is on) | the endpoint key the record describes | withhold, expire | point you at a different machine |
| Mailbox envelopes for offline people | the sender's BitName, sealed to the recipient | withhold, delay, drop | read, alter, or forge a sender |
So the worst a hub can do is fail to help, which is why several are configured at once and why any of them can be replaced.
A hub is itself a BitName. Its descriptor — roles, relay URL, retention, limits, operator — is published as that name's profile, so a hub is looked up and pinned exactly like a person, and Kleo can say "this is the hub you trusted last time" when its keys change.
Four roles, one small binary (kleo-resolver grows into kleo-hub, each role switchable):
- Resolver — hosts owner-signed profiles and answers
bitname_commit. Shipped. - Endpoint directory — signed address records keyed by endpoint id, so finding a
peer doesn't depend on a third party's DNS.
- Mailbox — store-and-forward envelopes for people who are offline.
- Relay — an
iroh-relayon the same host, advertised in the descriptor, so hole
punching and the fallback path don't depend on a third party either.
Defaults and choice. Kleo ships a short list of hubs (the ones published on the Kleo site among them), a name publishes its own preferred hubs in its profile, and both lists are editable. Running your own is one command, and a name that lists only its own hub never touches ours.
The honest cost. A hub sees IP addresses, which names collect mail, and the size and timing of envelopes — never content, never who wrote what. Direct peer-to-peer delivery stays the default and the mailbox is the fallback, so the metadata a hub sees is the traffic that couldn't be delivered directly.
[iroh]: https://iroh.computer
Phase 7: offline delivery — kleo-hub
Both sides no longer need to be online at the same time.
- Mailbox: a hub holds encrypted envelopes for a recipient who is away. Deposits must
be signed by a BitName (that is the spam barrier), fetching proves you hold the name by signing a challenge with its on-chain key, and envelopes expire on a published schedule.
- Endpoint directory and relay: the other two hub roles above, so Kleo can be pointed
entirely at infrastructure its users run.
- In the app: a hub list in Settings with the shipped defaults, a name's own hubs in
its profile, automatic fallback from a direct link to the mailbox, and a status line per conversation saying how the last message actually travelled.
- Also: delivery receipts, and reconnection that drains the mailbox on wake.
- Shipped: mailboxes (deposit signed by a BitName, collection proved by the
recipient's key, retention and per-name caps), the endpoint directory (records signed by the endpoint key, replay-proof, expiring), the hub descriptor, hub settings in the app, automatic fallback from a direct link to mail, and a background round that collects mail and republishes where this machine is.
- Shipped since: mailboxes can be addressed as
hash:<64 hex>, so a wallet restored
from its recovery phrase collects its own mail without anyone retyping a name.
- Not yet: delivery receipts.
Phase 8: everyday chat
The things that make chat usable daily, none of which need new cryptography.
- Notifications and background running: an OS notification for a new message, a tray
icon, and the option to keep receiving while the window is closed. Without this, a peer-to-peer message only arrives if the app happens to be open.
- Blocking and muting: refuse messages from a name outright, independent of the
pricing in phase 10.
- Attachments: send a file inside a conversation, reusing the phase 5 file encryption.
- Search and export: search conversations and contacts; export one conversation as
signed, verifiable text.
- A second machine: a name can publish more than one endpoint, and the vault (contacts
and history) can be exported and restored, so a new laptop doesn't mean starting over.
- Shipped: OS notifications for messages that arrive while Kleo isn't focused (the
text stays in the app), a tray icon and close-to-tray so messages keep arriving, blocking (local, never announced, dropped after decryption so the signature decides), attachments up to 150 KB sealed like .bnenc files and written into Kleo's own folder, search across conversations, plain-text export, and several devices per name (chat.endpoints, tried in turn).
- Shipped since: vault backup — one file holding contacts, pinned keys, blocks,
invitations, followed channels, your own names and every conversation, encrypted with Argon2id to a passphrase rather than to this machine's keychain. Importing only ever adds: contacts you already hold keep the keys you pinned, messages are matched by id, and restoring the same file twice changes nothing the second time.
- Not yet: a conversation export that can be re-verified, because Kleo keeps the text
it verified rather than the armor.
Phase 9: the wallet
Kleo can read a balance but cannot move a coin, and everything in phase 10 needs funds.
- Receive: addresses, with a QR for the address.
- Send: pay a BitNames address, with fee choice and a confirmation that names what is
being spent.
- Deposits and withdrawals: move coins between the mainchain and the sidechain through
the BIP300 enforcer, which is how a wallet gets funded in the first place.
- Transaction history, with registrations, profile updates and payments told apart.
- Shipped: balance, a fresh receiving address, a payment that is quoted and confirmed
before anything is signed (addresses have no checksum, so the address is shown back), deposits via format_deposit_address, withdrawals reported as started rather than sent, and a history assembled from the wallet's own outputs — upstream has no list-my-transactions call, so it is built from UTXOs, spent outputs and one get_transaction_info per transaction.
- Not yet: a QR code for the receiving address, and fee estimation (the fee is typed).
Phase 10: payments and paid messages
- Pay a BitName: look up the name, pick a payment method it publishes (BitNames,
Bitcoin or Paymail address), and send BitNames sats with create_transfer.
- Paid messages (paymail). Upstream already defines them:
- A paymail is a payment with a memo, sent to the address that holds the recipient's BitName. - The payment must be at least the recipient's on-chain paymail_fee_sats. - Kleo encrypts the memo to the recipient's key, because memos are public. - Its own inbox scanner replaces the broken get_paymail RPC.
- Pay to message someone (message requests):
- Each person sets their price: the on-chain paymail_fee_sats (price 0 means anyone may write). - Contacts, and people you've already talked to, are always free. - An allow list (decided): let specific BitNames through for free without saving them as contacts, so you can invite someone before they write. Entries can be one-off (their next message only) or permanent, and the list lives in the encrypted vault. - Charged once per stranger, not per message (decided). - A stranger's first message must carry a payment of at least that price. It arrives in a Requests folder, and the recipient's client checks the payment on its own node before showing it. Unpaid messages are dropped. - Accepting (replying, or saving the sender as a contact) opens a normal free conversation. - An optional one-click refund turns the fee into a deposit ("I'll refund you if it wasn't spam").
- Where "pay to talk" can apply:
| Case | Who pays | Mechanism | |---|---|---| | First DM to a stranger | sender | the recipient's price (paymail_fee_sats), before the first message | | Someone you invited | nobody | on your allow list, so they skip the price | | Joining a private channel | new member | the channel's join fee | | Posting in a busy community channel | poster | the channel's per-post fee |
- Shipped: paying a BitName (the address from their profile, or the one holding their
name), paid messages as upstream paymail — a payment whose memo carries the message, signed and sealed, hex on-chain — an inbox scanned out of the wallet because get_paymail returns nothing usable, prices published on-chain per name, an allow list with one-off and permanent invitations, and the gate itself: a stranger writing to a name that charges is refused with the price rather than dropped, and a free message from a stranger is dropped at the other end.
- Answered: a memo is hex and the chain accepted 16 KB in testing, so a paid message
carries the whole message rather than a pointer to it. Kleo caps it at 4 KB.
- Not yet: the one-click refund, and a Requests folder — a paid message lands in the
conversation it belongs to, marked with what was paid.
Phase 11: channels
A channel is itself a BitName. That makes channel names unique, human-readable and spam-proof in the same way people's names are. The channel's public profile (Phase 4) declares what kind of channel it is, its hubs, moderators and fees.
Order (decided): 11a → 11b → 11c.
- 11a Broadcast channels (one to many): announcements and feeds.
- The owner (and any authors they list) sign posts; followers fetch them from hubs or peers and verify every post against the channel's on-chain keys. - Public and cacheable anywhere, so they scale like profiles.
- 11b Private groups (small, end-to-end encrypted):
- Members are a signed list of BitNames. A group key is sealed to each member's BitNames encryption key. - Removing a member rotates the key. - Capped in size (for example 256 members): this is for teams and friends, not crowds.
- 11c Community channels (many to many):
- Any BitName holder can post, signed with their own key, so every post has a verifiable, costly-to-create author. - Moderators publish signed removal and ban lists, which hubs and clients honour. - Optional fees to post or join throttle abuse without accounts or CAPTCHAs.
Still out of scope: voice and video, media pipelines, and giant public groups that need server-side moderation.
- Shipped: all three kinds. A channel is declared in its own BitName's profile, so
what it is and who may post is covered by the on-chain commitment. Posts are signed over a statement naming the channel, author and time; the hub checks the author against the channel's rules before storing, and every reader checks the signature against the chain before believing it. Private groups seal a group key to each member's BitName and encrypt posts under it — changing the membership makes a new key, so a removed member reads nothing written afterwards — and communities carry published moderator and ban lists, so a ban is the channel's decision rather than a hub's.
- Not yet: fees to post or join, which need a payment checked per post; and posts
are fetched when a channel is opened rather than pushed.
Phase 12: release
- Installers for Linux, macOS and Windows, code signed and notarized, with an update
channel that can be rolled back.
- In-app onboarding: a first run today lands in Messaging with no wallet, no name
and no node. It should walk out of that.
- A written threat model that states what Kleo proves, what it only claims, and what
it leaks — the document that makes the promises on the site checkable.
- Reproducible builds and a dependency audit in CI.
- The bundle id is
xyz.kleoapp.desktop:kleoapp.xyzreversed, which is the
domain we actually hold. The app name is still open.
- Shipped: a release workflow that builds an installer for macOS, Linux and Windows
on a tag and drafts a release, a dependency audit in CI, the threat model (docs/THREAT_MODEL.md), and in-app onboarding that says which of node, wallet, name and hub is still missing.
- Not yet, and needing a decision or a secret: code signing and notarization (needs
certificates), an update channel with rollback, reproducible builds, and the final app name. The bundle id is settled: xyz.kleoapp.desktop.
The one that changes the shape: a light client
Today Kleo has no keys. Signing, decryption and the seed live in the node's wallet, and Kleo drives it over RPC. That is why everyone needs a node of their own: a remote node means that node's wallet is your identity, and two people pointed at the same node are one wallet with two windows.
The alternative is for Kleo to hold its own seed, derive its BitName keys, and do the signing and encryption itself — using a node only as a view of the chain. Then a remote node has to be honest, not trusted with secrets, and one public node could serve everybody who wants to look something up.
What it takes:
- Deriving BitName keys from a seed Kleo holds, in the same scheme the node uses, so a
wallet can move between the two.
- Building and signing sidechain transactions locally — registrations, transfers,
paymail. Kleo already builds BitNameUpdate transactions and only asks the node to sign them, so the shape is known; what is missing is the signing and the UTXO selection.
- Somewhere to keep the seed: the vault, behind the lock that already exists.
- Deciding what a dishonest read-only node can still do to a client, and saying so.
Worth doing before asking strangers to install anything, because "run three daemons first" is the current answer to "how do I try it", and it is not a good one.
Cross-cutting, not scheduled
Work that belongs to no single phase and should land alongside whichever one touches it.
- Node bootstrap. plain-bitnames has no DNS seeds: peers live in a
known_peersDB
and are added by hand with connect_peer. Kleo only lists peers, so a fresh node has nobody to talk to. The Node page should be able to add, forget and suggest peers.
Forward secrecy.Shipped: a name publishes an X25519 prekey in its profile, and
conversations between two names that have one run a Double Ratchet — a key per message, discarded after use, with the BitName signature still inside deciding who wrote it. A conversation upgrades silently the moment the other side publishes a prekey, and falls back when they haven't. Rotation shipped too: replacing a prekey is an ordinary profile update, the replaced key is kept so mail already sealed against it still opens, and a message that opens a session Kleo doesn't have starts one. Still to do: extending the same treatment to files and channel posts.
Locking the app.Shipped: an optional passphrase wraps the vault key and takes
it out of the keychain, with a lock-now button and an idle timer. Locking clears contacts, history and chat keys from memory. Still to do: locking on sleep, and a shorter grace period than the idle timer for a screen someone walks away from.
- Accessibility and translation. A keyboard and screen-reader pass, then the strings
extracted for translation.
- Name lifecycle. What Kleo should do when a name changes hands mid-conversation, and
whether names can expire, is unanswered upstream and needs a decision before channels.
- Supply chain.
cargo audit/cargo vetand a pinned, reviewed dependency set in CI.
How spam is kept out
| Surface | Barrier |
|---|---|
| Hosting a profile on a resolver | Must be signed by the name's on-chain key |
| DM from a contact | None (you chose them) |
| DM from a stranger | Their BitName plus your price (paymail_fee_sats) |
| Broadcast channel | Only the owner and listed authors can post |
| Private group | Members only, end-to-end encrypted |
| Community channel | A BitName to post, optional post fee, moderator ban lists |
| Hubs (mailbox) | Accept only envelopes signed by a BitName; apply per-name limits |
Open decisions
Answers are needed before the phase in brackets:
- Chat history [6–7]: kept on your computer, encrypted, with no expiry by
default, plus a per-conversation "delete" and "keep for N days". Mailboxes (phase 7) stay optional and off unless you name a hub. Revisit once chat is usable.
- Refunds for paid messages [10]: whether the fee can be refunded with one click.
Proposed: yes, since it turns a toll into a deposit.
Paymail memo limits— answered: memos are hex, and a real regtest node
accepted 16 KB. Kleo puts the whole message in, capped at 4 KB.
- Mailbox retention [7]: how long a hub holds an undelivered envelope, and what it
does when a mailbox fills up. Proposed: 30 days and a per-name size cap, both published in the hub's descriptor so a client can see the terms before using it.
Decided: transport is iroh, peer-to-peer first; Kleo runs its own hubs as seed nodes but never trusts them; paid messages are charged once per stranger, with an allow list for people you invite; channels ship in the order 11a → 11b → 11c.