WEFT Protocol

Draft

WEFT 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.

01

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.
02

Data Model

Logs

prev prev genesis seq 0 · signed device add seq 1 · signed rotation seq 2 · signed head

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:

  • MESSAGE signs plaintexts and ordinary statements.
  • ENROLL adds another device.
  • CONFIG changes identity configuration, including its home relay.
  • MODERATE signs 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
03

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
04

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.

sealed bytes route: 7d19… open signed plaintext room: cedar… sender: nadia… “Meet under the cedar?” signed by Nadia’s phone

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.
05

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.

:) Nadia local contacts: Omar :D Omar local contacts: Nadia Omar may deposit granted by Nadia's local decision Nadia may deposit granted by Omar's local decision no shared object

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:

  1. heads
    Authority news first. Log heads, fork proofs, revocations, and missing tails. Self-verifying objects may flow broadly.
  2. statements
    Expiring claims second. Bag contents filtered to mutual interest and admitted by trust, vouchers, or room scope.
  3. envelopes
    Mail 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.

06

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.

07

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.

  1. 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.

  2. 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.

  3. 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’s route_commit.

  4. 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)
    }
  5. 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.

  6. 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.