WEFT Protocol
DraftWEFT is a decentralized chat protocol based on networks of trust. It has no global authority, no blockchain, supports end-to-end encryption and decentralized identity, and works in relay-based settings and peer to peer meshes.
Data Model
Core Concepts
The protocol deliberately keeps the number of core concepts small. There are only 5.
- Log
- A signed hash-chain carrying slow-updating identity or governance changes. People, communities, and rooms are all represented as logs.
- Statement
- A signed claim that expires. Statements exist freely and are not included in any hash-chains. They can be created concurrently, coexist, and expire independently. Membership, names, presence, introductions, and custody are represented as statements.
- Envelope
- An opaque route token and ciphertext containing one room-bound, device-signed section of plaintext. The route token identifies the room's mailbox slot without identifying its sender or recipients.
- Node
- Anything that authenticates and speaks WEFT sync: phone, laptop, relay, host, or courier.
- Edge
- Two identities' reciprocal decisions to admit each other's general traffic. Edges exist locally, but may be shared with relays via vouchers.
Other Terms
- Relay
- A node that promises to stay awake, validate and store an identity’s log, and maintain its mailbox while its devices are offline. Each identity identifies one “home” relay. A room may separately identify a relay as its host.
- Room
- A log representing a conversation. Every conversation is a room, including two-person chats. A room defines its rules and moderators, while its current members are represented by statements.
- Community
- A log representing a collection of rooms. Communities grant moderators, publish room directories and membership statements, and may identify a relay as their host. A community is a room-kind log, not a server.
- Device
- A signing and encryption key delegated by an identity to act on its behalf. Devices may be given different capabilities.
Data Model
Logs
A Log is an append-only chain of signed events.
Each log chooses one of two authority models: an identity log delegates
devices, while a room log grants capabilities to identities. The hash of a log's
genesis event is its permanent DID (decentralized identifier):
did:weft:<base32(blake3(genesis_event))>
Every later event names the preceding event by hash and increments its sequence number. A verifier can therefore replay a log offline and reconstruct exactly one current state:
event {
seq
prev
type
body
sig
}
Each log event carries a type. identity and room logs support different
types, but share the following:
| Event | Does | Key material |
|---|---|---|
genesis |
Chooses the permanent log kind, declares the first root, and commits to the next one. | Declared rotation key |
config |
Changes slow-moving relay pointers or room policy. | Kind-specific |
rotate |
Replaces the root, heals a fork, or ends the log. | Committed next root |
Why precommitments?
Every log contains a commitment, which is the hash of the public key that must become its next root. The matching private key is generated in advance and kept separately. When the log rotates, it reveals the committed public key, proves possession by signing the rotation with its private key, and commits to another future root. The current root therefore cannot simply invent its successor later. For identities, this means a thief holding every live device key still cannot rotate, heal, or kill the log: recovery authority was chosen before the compromise. Lose the committed private key, however, and the log can no longer rotate.
Identity Logs
An identity log represents a person, service, relay, or other direct speaker. Its authority table contains devices. Devices sign messages and statements as that identity. Identity logs support the following events:
| event | purpose | authority |
|---|---|---|
delegate |
Adds a signing and KEM key with explicit capabilities. | Identity root or ENROLL device |
revoke |
Removes a device immediately. | Identity root or any delegated device |
Each device may hold any of the following capabilities:
MESSAGEsigns plaintexts and ordinary statements.ENROLLadds another device.CONFIGchanges identity configuration, including its home relay.MODERATEsigns personal trust vouchers and may exercise matching room grants.
Room Logs
A room log represents a conversation or community. It grants authority to a person or service as a whole. That identity's own log determines which of its phones or computers may use it. Room logs support the following events:
| event | purpose | authority |
|---|---|---|
grant |
Adds one identity and its capabilities. The DID must not already be present. | Room root only |
ungrant |
Removes that identity’s whole grant row. | Room root only |
Data Model
Statements
A statement is something an identity, room, or community says is true for a limited time. Statements sit outside logs because they do not change who may speak or how a room is governed. Many can be issued at once, and each disappears unless it is freshly signed again.
statement {
issuer // identity, room, or community whose claim this is
actor? // required only when issuer is a room or community
subject // person, room, or value the claim is about
claim {
kind
...
}
issued
expires
sig // signature from the authorised key
}
Why separate issuer and actor?
An identity normally signs statements with one of its device signing keys. A room has no device
keys of its own, so a granted moderator signs on its behalf using one of their device keys.
issuer names the room whose claim this is, and actor names the
moderator whose device signed it. The room root signs log events, never statements.
The following kinds of statement are supported:
| kind | what it says | special rule |
|---|---|---|
voucher |
A subject is admitted by a named audience. | Audience-bound; self-audience requires MODERATE, introductions
MESSAGE. |
name |
An issuer attests a human-readable name. | Consumer judges control of the scope. |
successor |
A new DID succeeds a frozen or dead one. | Social evidence; humans decide. |
presence |
The issuer is online now. | Issuer equals subject; horizon ≤ 5 minutes. |
push |
A device has a wake endpoint. | Issuer equals subject. |
channel |
A community names one of its rooms. | Community authority requires MODERATE. |
share |
Someone holds a recovery share. | Custody notice only. |
kick |
A room has removed a member. | Room-issued; an actor with MODERATE is required. |
How a statement is checked
Each statement kind requires a capability. The verifier follows one of two paths using the latest log state it knows:
identity statement
issuer's identity log
→ signing device has the required capability
room or community statement
issuer's room-kind log
→ grants actor the required capability
→ actor's identity log
→ signing device has the same capability
Data Model
Envelopes
An envelope is the object that represents messages in WEFT. Inside is a
route and encrypted ciphertext. The ciphertext opens to a signed plaintext
containing the room, sender, sending device, display-order hint, and body.
When a room is created, it generates a random route secret. That secret produces the opaque route token carriers use as the room's mailbox address. The token stays the same for the life of the room.
A new member receives the route secret privately when they join. The room log stores a hash of the real secret, so the new member can check that what they received is correct. An inviter therefore cannot strand them with a different token that nobody else is using.
Per-device epoch keys
An epoch is one generation of a device's sending key for one room. Membership changes create new epochs. When a member leaves, each remaining device creates a new epoch before it next sends and does not give them the new key. When a member joins, each sender creates a new epoch so they cannot read earlier messages. Rooms with permanent history instead give new members the current keys.
Each epoch key is encrypted separately to every permitted device using HPKE. Devices manage their own epochs independently: nobody needs to be online at the same time, and there is no room-wide key exchange. The first epoch delivery to a new member also contains the room's route secret.
| change | sender action before next message |
|---|---|
| Join into finite retention | Rotate, then encrypt and deliver the new epoch key to the newcomer. |
Join into forever |
Encrypt and deliver the current epoch key; do not rotate. |
| Own device revoked | Each remaining device rotates immediately. |
| Another member device revoked | Rotate and exclude that device. |
| Membership lease expires | Rotate and exclude the former member’s devices. |
Data Model
Nodes & Edges
A node is any device running WEFT. An edge is mutual personal trust stored as local state. There is no global friend list.
An edge is not a signed global object with two names on it. Nadia records “I trust Omar as a contact” on her own devices; Omar separately records the reciprocal decision. The protocol calls the relationship an edge only when both local decisions exist.
Each person materializes their decision to their own home relay as an ordinary audience-bound voucher. Nadia's half looks like this:
nadia_admits_omar = {
issuer: "did:weft:nadia",
subject: "did:weft:omar",
claim: {
kind: "voucher",
audience: "did:weft:nadia",
subject_head: hash(omar_head_nadia_observed)
},
issued: 1730001000,
expires: 1732593000,
sig: sig_by(nadia_phone_sign)
}
Why edges matter
A route token tells a recipient which sealed bytes it knows how to claim. It does not tell a relay who should be allowed to consume storage and bandwidth by handing it bytes. Edges provide that missing social admission boundary.
| state | what may cross the relay boundary | what it does not mean |
|---|---|---|
| strangers | Authenticated, rate-limited bootstrap knocks only. | Knowing a DID is not mailbox admission. |
| co-members, no edge | Only envelopes and room-issued statements for the shared room, with both membership proofs. | Joining a 5,000-person community does not make 4,999 contacts. |
| one-way contact | If Nadia admits Omar, Omar may deposit to Nadia; the reverse direction remains closed. | One voucher is not yet a mutual edge. |
| mutual edge | Ordinary admitted sync and mailbox ingress in both directions. | An edge still grants no room membership or decryption key. |
Relays and couriers
A home relay is the node an identity asks to stay online on its behalf. It stores the identity's log, accepts an update only when it extends the head already stored, and holds route-addressed envelopes until its devices collect them. An identity names exactly one home relay in its current config.
A courier carries opaque envelopes between disconnected parts of the network. It advertises the route tokens it holds, and nodes claim the envelopes whose tokens they recognise. Envelopes are identified by hash, so receiving the same one through several couriers does not duplicate it. Couriers provide delivery, not ordering.
Node to node communication
When two nodes meet, they authenticate using currently delegated device keys and run sync in the following order:
-
headsAuthority news first. Log heads, fork proofs, revocations, and missing tails. Self-verifying objects may flow broadly. -
statementsExpiring claims second. Bag contents filtered to mutual interest and admitted by trust, vouchers, or room scope. -
envelopesMail last. Route-token claims, matching envelopes, and attachment bytes requested by hash.
QUIC, BLE, and removable storage are dumb pipes. The same sync semantics apply to every transport.
Network
Naming & Communities
Discovery, reference, and vanity are different jobs. WEFT does not force all three into one globally scarce string.
- share-codes
- A rendezvous primitive containing a DID and relay hint. Holding one lets you knock, not enter the mailbox proper.
- petnames
- Local names for contacts. A name in Bob's message resolves through Bob's table and is rendered with that provenance.
- scoped names
- Attestations such as
alice.example.com, checked against the administrator of that namespace. Losing the name does not lose the DID. - introductions
- Audience-bound vouchers from a mutual contact, carrying the introducer's current view of the subject's head.
Communities
A Community is a room-kind log, not a server. Its log grants identities moderation capabilities, its statement bag carries a channel directory and member vouchers, and its config names the current host.
Network
End to end example
Nadia joins the Bakery community's sourdough room. She meets Omar there, adds him as a contact, and later starts a private two-person room. This trace shows which protocol object exists at each step.
-
step 1 Discover the room through the community.
Nadia first resolves the Bakery community log. Its grant table names the moderator identity; that identity’s current log names the devices allowed to exercise
MODERATE. The directory entry itself is detached:channel_statement = { issuer: "did:weft:bakery-community", actor: "did:weft:bakery-moderator", subject: "did:weft:sourdough-room", claim: { kind: "channel", label: "sourdough" }, issued: 1730000000, expires: 1737776000, sig: sig_by(community_moderator_sign) }Why a statement? The community can publish many rooms concurrently; directory entries may be renamed, replaced, or allowed to decay without appending governance history.
-
step 2 Receive community and room membership.
A community may maintain its own membership policy, but that does not automatically grant access to every room. The community and room issue separate, scoped vouchers:
community_membership = { issuer: "did:weft:bakery-community", actor: "did:weft:bakery-moderator", subject: "did:weft:nadia", claim: { kind: "voucher", audience: "did:weft:bakery-community", subject_head: hash(nadia_head) }, issued: 1730000000, expires: 1732592000, sig: sig_by(community_moderator_sign) } room_membership = { issuer: "did:weft:sourdough-room", actor: "did:weft:room-moderator", subject: "did:weft:nadia", claim: { kind: "voucher", audience: "did:weft:sourdough-room", subject_head: hash(nadia_head) }, issued: 1730000010, expires: 1730604810, sig: sig_by(room_moderator_sign) }The second statement is the one every host or recipient checks before accepting a sourdough-room message from Nadia.
-
step 3 Bootstrap the room secret and a sender epoch.
Nadia has no previous sourdough-room key. Omar’s phone sends an epoch distribution on Nadia’s bootstrap route. The HPKE seal contains both Omar’s sender key and the room route secret:
epoch_plaintext = { channel: "did:weft:sourdough-room", sender: "did:weft:omar", device: omar_phone_sign, seq_hint: 81, body: { kind: "epoch", epoch: 7, seals: { hash(nadia_phone_sign): HPKE_Seal( recipient = nadia_phone_kem, info = "weft:epoch:v1", aad = { channel: "did:weft:sourdough-room", epoch_id: ["did:weft:omar", omar_phone_sign, 7], device_id: hash(nadia_phone_sign) }, plaintext = { key: omar_room_epoch_7, route: sourdough_route_secret } ) } }, sig: sig_by(omar_phone_sign) } bootstrap_envelope = { route: derive_route(open_key("route", nadia_did)), ciphertext: AEAD_Seal( key = open_key("epoch", nadia_did), aad = route, plaintext = epoch_plaintext ) }Nadia accepts the route only after confirming that
blake3(sourdough_route_secret)matches the room log’sroute_commit. -
step 4 Participate as an ordinary member.
Nadia can now claim the room route and send her own signed, room-bound plaintexts. In a hosted room, the host assigns a stamp after checking her membership:
message_plaintext = { channel: "did:weft:sourdough-room", sender: "did:weft:nadia", device: nadia_phone_sign, seq_hint: 12, body: { kind: "msg", text: "Does anyone have a rye starter?" }, sig: sig_by(nadia_phone_sign) } room_envelope = { route: derive_route(sourdough_route_secret), ciphertext: AEAD_Seal( key = nadia_room_epoch_0, aad = route, plaintext = message_plaintext ) } host_stamp = { room: "did:weft:sourdough-room", term: hash(room_config_naming_bakery_host), seq: 284, envelope: hash(room_envelope), sig: sig_by(bakery_host_device) } -
step 5 Nadia decides to trust Omar personally.
Sharing a room did not automatically create a social edge. Nadia resolves Omar’s current identity log, then materialises her own contact decision to her home relay:
nadia_admits_omar = { issuer: "did:weft:nadia", subject: "did:weft:omar", claim: { kind: "voucher", audience: "did:weft:nadia", subject_head: hash(omar_head_nadia_observed) }, issued: 1730001000, expires: 1732593000, sig: sig_by(nadia_phone_sign) }This voucher tells Nadia’s relay that an authenticated session whose DID is Omar may deposit ordinary statements and envelopes into Nadia’s mailbox. It grants nothing to anyone else.
Omar independently makes the reciprocal decision:
omar_admits_nadia = { issuer: "did:weft:omar", subject: "did:weft:nadia", claim: { kind: "voucher", audience: "did:weft:omar", subject_head: hash(nadia_head_omar_observed) }, issued: 1730001060, expires: 1732593060, sig: sig_by(omar_phone_sign) }The edge exists only after both local decisions. The two vouchers are not a global friendship record and need not share a transaction or identifier.
-
step 6 Optionally create a private two-person room.
Becoming contacts and creating a room are separate actions. If Nadia starts a private chat, her client creates an ordinary hostless room log and two room-scoped membership statements:
private_room_genesis = { seq: 0, prev: null, type: "genesis", body: { kind: "room", rotation_key: private_root_0, next_commit: blake3(private_root_1), grants: [{ did: "did:weft:nadia", caps: MODERATE }], config: { tier: "e2e", lease: 1209600, route_commit: blake3(private_route_secret) // no relay → hostless } }, sig: sig_by(private_root_0) } nadia_membership = voucher( issuer = private_room, actor = Nadia, subject = Nadia, audience = private_room, expires_in = 14_days ) omar_membership = voucher( issuer = private_room, actor = Nadia, subject = Omar, audience = private_room, expires_in = 14_days )Nadia’s first epoch distribution to Omar uses his bootstrap route because the new room has no previous shared epoch. Once he accepts, the private room’s own membership proofs and route carry subsequent traffic.
What changed where?
| action | object | permanent log append? |
|---|---|---|
| Community appoints a moderator identity | grant event |
Yes—changes community authority. |
| Community publishes a room | channel statement |
No—directory claim may decay. |
| Nadia joins community / room | Two voucher statements |
No—scoped memberships renew independently. |
| Omar gives Nadia room keys | epoch envelope |
No—message history. |
| Nadia chats | msg envelope + host stamp |
No—message history. |
| Nadia trusts Omar | Self-audience contact voucher | No—local relationship may lapse. |
| Nadia starts private chat | New room genesis | Yes—a new room constitution. |
| Nadia and Omar join private chat | Two room membership vouchers | No—current member set. |