Identity and access
decided in principle, draft in detail.
A user is a public key
Section titled “A user is a public key”XTXP identifies a user by a public key and authenticates them by proof that they hold the matching private key. There are no accounts on a central server and no email-like addresses. This follows the same idea as SSH keys and Nostr, where the key is the identity.
Getting the key onto a device. People shouldn’t have to manage key files. Signing in with an account they already have (Google, Apple or Mozilla) fetches their private key from their own storage. XTXP never holds it. The sign-in provider is just a way to get to the user’s key, not the identity itself.
No-JS browsers. A browser without JavaScript can’t hold a key. For now, the server-side muppet acting for that user generates an ephemeral key tied to their HTTP session, perhaps kept across visits by a cookie, and the document’s owner grants that key access (decision 23). The server never holds the user’s own key. Linking a device key to the user’s key comes later.
Signing only at identity steps
Section titled “Signing only at identity steps”Signing every operation would be heavy, so XTXP signs only the steps that establish who is acting:
- Connecting. The host sends a random challenge and the peer signs it, proving it holds the key. The peer signs the challenge together with the host’s name and the session address, so a malicious host can’t pass the challenge on and use the answer to sign in somewhere else as that user.
- Binding a client ID. Every Xmesh operation already carries a client ID. A peer signs one statement, “public key P owns client 7 on session
{host}/{path}@{name},” and from then on every operation from client 7 is attributed to P. A client ID is fresh for every session and every connection: a fork shares item IDs with its parent, so one client ID used on two sessions would make two different items with the same ID, and a merge would silently drop one. A host rejects a binding for a client ID already anywhere in the session’s history, including history it inherited from its parent. Yrs picks client IDs at random, so a fresh one costs nothing. The redline gets authorship with no per-operation cost. - Granting and revoking. Changes to who may read or write are signed by the person making them, so anyone can check them.
Operations themselves travel unsigned, over an authenticated connection. Cursors and presence are tied to that connection too, so nobody can show a cursor as someone else.
What gets signed
Section titled “What gets signed”decided (decision 22)
Keys are Ed25519. Every signature covers a domain tag naming what kind of statement it is, so a signature made for one purpose can never be reused for another:
| Statement | Domain tag | Also covers |
|---|---|---|
| Answering a challenge | xtxp-auth-v0 |
the host’s name, the session address, the nonce |
| Binding a client ID | xtxp-bind-v0 |
the client ID, the session address |
| Granting or revoking | xtxp-grant-v0 |
the key, the access level, the session address |
Access is granted by key
Section titled “Access is granted by key”- Whoever creates a session owns it.
- The owner grants
readorwriteto a specific public key, or to*for everyone. - Forking needs read access, and the forker owns the fork, as with forking a repository on GitHub.
- Merging a session back needs write access on the parent.
- A session’s host enforces its grants when operations arrive.
- The owner also decides which sessions this session trusts, so their changes merge into it automatically (trust between sessions).
This lines up with sessions as history, and it covers ordinary websites without special cases: @origin is readable by * and writable by no one. Anyone can fork it, and each fork belongs to whoever made it.
Revocation closes the door on arrival
Section titled “Revocation closes the door on arrival”Once a host has received a revocation, it admits nothing new from that key on that session.
Each host closes the door on its own sessions. A revoked key’s edits that were already admitted on another session can still arrive through an automatic merge, if this session trusts that one. To keep them out, the owner withdraws trust from the session they come from. Revocation is rare, so that is a fair price for letting sessions on different hosts keep each other up to date.
The rule depends on when the revocation arrives, not on where an operation claims to sit in history. A revoked user could otherwise backdate edits by claiming an earlier state vector, and those would be indistinguishable from honest offline work.
Edits that arrive too late aren’t thrown away. Since admission is by session, the revoked user’s session simply stops being merged. It stays theirs, so nothing they wrote is lost and nothing more reaches the protected session.
Still open
Section titled “Still open”- Where the key lives and how it’s protected in the user’s storage. One option is to encrypt it with a key derived from a passkey (the WebAuthn PRF extension), so the storage provider can’t read it. That has its own risk: losing the passkey means losing the key.
- Admin rights. If only the creator can grant, losing their key freezes the session. A grantable
adminrole would fix that. - Several devices, one person. A personal root key that signs per-device keys would keep one identity across devices, and allow revoking a lost device.
- Provenance beyond the host. With signatures only at identity steps, a dishonest host could forge operations under a client ID. A later, cheap fix: each client hash-chains its own operations and occasionally signs the chain head.
- Key recovery when every device and the backup are lost.