15 min read essay protocol decentralization

The Loom You Can Read

The Luddites didn't fear machines — they refused looms they weren't allowed to open. Follow one note, “Buy milk,” all the way through xNet's internals: a file on your own disk, a signed change log, a name you mint instead of an account, and a three-line merge that settles conflicts with no server in the middle. A guided tour of a machine you're allowed to open — written for developers and everyone else at once.

The Luddites have a bad reputation they didn’t earn. We use their name for anyone who’s afraid of technology, but the real weavers of 1811 weren’t afraid of machines — they owned and ran complicated ones every day. What they refused was a particular machine: the power loom installed by a factory owner, in a building they couldn’t enter, running rules they weren’t allowed to see, built to make their own skill worthless. Their fight was never machine versus human. It was about who the machine is for, and whether the worker is allowed to open it.

Two hundred years later you are the weaver, and almost every app you touch is that sealed loom. You can feed it and you can watch it run, but you may not open the cabinet, read the mechanism, or take the cloth and leave. This essay is about a loom built the other way — one you’re allowed to open. To show you it’s real, we’re going to follow a single, boring thing all the way through the machine: you sit down at your laptop and type a note that says Buy milk. By the end you’ll have seen every place that note lives, and every place an ordinary app would quietly betray it. There’s a little code along the way — you can skip every grey panel and the story still holds — but it’s there because being able to show you the machine is the whole argument.

One: it’s already on your disk

The instant you finish typing Buy milk, where does it live? In a normal cloud app, the honest answer is: somewhere else. Your keystrokes are sent off to a company’s server, which holds the real copy and lends you a view of it. “The cloud” is just someone else’s computer, and on that computer your note is a guest that can be read, ranked, monetised, or locked out — and if the network drops, so does your note.

xNet starts at the opposite end. Your note is written first to a small database — SQLite, the same engine inside your phone — that lives in a private corner of your own browser called the Origin Private File System. No server is consulted. No round-trip happens. The note is yours, locally and completely, before the internet is even part of the story. The screen updates instantly because it’s reading from a file a few millimetres away, not a data centre an ocean away. Turn off your Wi-Fi and nothing changes; the app was never really talking to the network to begin with.

flowchart LR
  subgraph CLOUD["The usual way — cloud-first"]
    direction TB
    U1["You type"] -->|"round-trip"| S["A company server<br/>(the real copy)"]
    S -->|"if online"| V1["Your screen"]
    S --> K["They keep a copy<br/>they can read and lock"]
  end
  subgraph LOCAL["xNet — local-first"]
    direction TB
    U2["You type"] --> DB["SQLite on your disk<br/>(the real copy)"]
    DB --> V2["Your screen — instant, offline"]
    DB -.->|"later, optional"| H["A hub<br/>(may not even be able to read it)"]
  end
Same keystroke, two architectures. Cloud-first makes the server the source of truth and you the guest. Local-first makes your disk the source of truth and the network optional.

This one inversion — your device holds the master copy — is the foundation everything else is bolted to. It’s the difference between renting a seat in someone’s building and owning the loom in your own front room. But a loom in your front room raises an obvious question: if the real copy is on your laptop, and another real copy is on your phone, how on earth do the two ever agree? To answer that, we need to look at what a single edit actually is.

Two: every edit is a little signed receipt

Here is the first idea worth slowing down for. xNet never stores your note as a note. It stores the history of edits that produced it — and each edit is a tiny, self-contained record called a change. If you’ve ever used the version-control tool git, you already have the mental model: nothing is ever overwritten in place; instead every change is a sealed receipt that says what changed, who changed it, and which receipt came right before it. Your note is just the sum of its receipts, replayed in order.

What’s actually inside one of these receipts is short enough to read in one sitting.

Open the panel — what a single edit looks like
packages/sync/src/change.ts
export interface Change<T = unknown> {
  id: string  // a unique id for this edit
  payload: T  // just the fields that changed
  hash: ContentId  // "cid:blake3:…" — this edit’s fingerprint
  parentHash: ContentId | null  // the previous edit’s fingerprint
  authorDID: DID  // who made it
  signature: Uint8Array  // Ed25519 — proof it was them
  lamport: number  // a logical clock, for ordering
}
Every edit points at the one before it by fingerprint, and is signed by its author. So the whole history is a tamper-evident chain — exactly like git's commits, but for everything you make.

Two of those fields are doing quiet, heavy work. The hash is a fingerprint of the edit’s contents, produced by a fast modern hash function (BLAKE3). Change a single character and the fingerprint changes completely. And because each edit also records the fingerprint of the one before it, the receipts form a chain: tampering with any past edit changes its fingerprint, which breaks the link in every edit that followed. You can’t quietly rewrite history; the seams show.

flowchart LR
  C1["Edit #1<br/>hash a1b2<br/>parent: none"] --> C2["Edit #2<br/>hash c3d4<br/>parent a1b2"]
  C2 --> C3["Edit #3<br/>hash e5f6<br/>parent c3d4"]
  C3 -.-> N["Change any past edit and its hash changes,<br/>so every later edit&rsquo;s parent link breaks.<br/>Tampering is obvious."]
Each edit names its parent by fingerprint. That single link is what makes the past tamper-evident — and it's the same trick that secures a git repository or a blockchain.

The other field is the signature. Before an edit is stored, it’s signed with a private key only you hold (using Ed25519, the same kind of signature that secures SSH and modern messaging apps). Anyone, anywhere, can later check that signature and know the edit really came from you and wasn’t altered in transit — without ever needing to ask a server “is this person who they say they are?” The proof travels with the data.

Open the panel — sealing the receipt
signChange() — packages/sync/src/change.ts
const hash = computeChangeHash(unsigned)  // canonical JSON → BLAKE3
const signature = sign(toBytes(hash), signingKey)  // Ed25519, deterministic
return { ...unsigned, hash, signature }
Hash the contents, then sign the hash. Because the signature scheme is deterministic, the same edit produces byte-identical bytes on any device, in any language — which is what makes the next two sections possible.

Three: your name is a key, not an account

That signature only means something if “you” means something. On the normal web, your identity is an account — a row in a company’s database that they create, control, and can delete. Your name on their loom is theirs to switch off.

xNet has no accounts. Your identity is a cryptographic key pair you generate on your own device, and your public name — your DID, or decentralised identifier — is literally your public key, written out as text. Nobody issues it. There’s no registrar, no username server, nothing to revoke. It looks like did:key:z6Mk… and the recipe that produces it is small enough to print.

Open the panel — minting a name nobody can revoke
createDID() — packages/identity/src/did.ts
const ED25519_PREFIX = new Uint8Array([0xed, 0x01])

export function createDID(publicKey: Uint8Array): DID {
  const bytes = new Uint8Array([...ED25519_PREFIX, ...publicKey])
  return ('did:key:' + base58btc.encode(bytes)) as DID  // did:key:z6Mk…
}
Your public key, tagged and text-encoded. That's the whole identity. Because it's self-certifying, any device can verify your signatures with nothing but the name itself — no central directory to phone home to.

This is the hinge the whole “you can leave” promise turns on. An account lives on the platform; lose the platform and you lose the name. A key lives with you; it works on any hub, on any device, with any app that speaks the protocol. Your name is something you carry, not something you’re lent.

Four: two devices, no referee

Now we can answer the question from the start. Your laptop and your phone each hold a real copy of your note. Suppose you’re on a train with no signal, and on your laptop you rename the note to Groceries — while, in your pocket, your phone (also offline) had already renamed the very same note to Shopping. Two edits, same field, no connection between them. When you get home and both reconnect, who wins?

In a cloud app the server decides, because the server is the boss. xNet has no boss. Instead, every device runs the exact same rule to pick a winner — and because the rule is simple and deterministic, they all reach the same answer without anyone refereeing.

sequenceDiagram
  participant L as Laptop (offline)
  participant H as Hub (relay)
  participant P as Phone (offline)
  L->>L: rename note to "Groceries" (lamport 7)
  P->>P: rename note to "Shopping" (lamport 7)
  Note over L,P: both come back online
  L->>H: signed change — title = "Groceries"
  P->>H: signed change — title = "Shopping"
  H-->>P: deliver the laptop's change
  H-->>L: deliver the phone's change
  Note over L,P: the same 3-line rule runs on BOTH devices —<br/>lamport ties, wallTime ties, higher key wins —<br/>both land on the same title, with no server vote
No central tie-breaker. Each device receives the other's signed change and runs the identical rule, so both converge on the same result on their own.

The rule is called last-write-wins, and it’s genuinely three lines. Compare the two edits’ logical clocks (a counter that captures cause-and-effect); if those tie, compare wall-clock time; and if those somehow tie too, fall back to comparing the authors’ keys themselves. That last line looks almost silly, but it’s the point: there is always a deterministic answer, so every device — yours, mine, one written in a different programming language entirely — lands on the same winner.

Open the panel — the rule that decides, in full
packages/data/src/store/store.ts
shouldReplace(existing, incoming): boolean {
  if (incoming.lamport  !== existing.lamport)  return incoming.lamport  > existing.lamport
  if (incoming.wallTime !== existing.wallTime) return incoming.wallTime > existing.wallTime
  return incoming.author > existing.author  // last resort: compare the keys themselves
}
Three comparisons, no network, no authority. Because it's pure and total, running it anywhere gives the same result — that's the whole meaning of 'no referee'. (The loser isn't lost, either: it's still in the history, just not the current value.)

Sit with how strange and good that is. The most contentious moment in any shared system — two people changed the same thing at once — is resolved here by a rule you can read in ten seconds, running independently on every device, owned by no one. There is no server in the middle adding its thumb to the scale. That’s not a missing feature. It’s the feature.

Five: the hub is a post office, not a landlord

“But wait,” you might say, “my laptop and my phone clearly did talk to something to swap those edits.” They did. It’s called a hub, and the most important thing about it is how little it’s trusted. A hub takes signed changes, puts them in order, and forwards them to your other devices and anyone you’re collaborating with. It’s a post office: it routes the mail and keeps things moving. It is emphatically not the landlord that owns your data.

The difference is enforced by the machinery we’ve already walked through. The hub can’t forge an edit, because it can’t produce your signature. It can’t secretly rewrite your history, because the fingerprint chain would break and every device would notice. And on the encrypted path, it can’t even read your content, because the key to unlock it is wrapped individually for each recipient and the hub isn’t one of them. Here’s the exact line between what it can see and what it can never do.

Where the trust line falls

The hub is trusted for delivery and ordering — not for truth, and not for privacy. Here is exactly what that means.

What the hub can see

  • The order of changes, and the rooms they belong to — it has to, to relay them.
  • Who authored each change (their public did:key) and when.
  • On the default, unencrypted path: the property values it forwards.

What the hub can never do

  • Forge your signature. Every change is Ed25519-signed; a tampered one fails verification on every device.
  • Rewrite history. Each change names its parent by hash, so silently editing the past breaks the chain for everyone.
  • Read content on the encrypted path. There, the content key is wrapped per-recipient (X25519), so the hub stores ciphertext it has no key for.

Swap the hub for another one — or run your own — and nothing about your data changes. That is the difference between a relay and a landlord.

We’re going to be honest below about where that line sits today — the default path still relays values the hub can read, and only the encrypted path makes it fully blind. But the structure is the point: a hub is replaceable. Don’t like yours? Move to another, or run your own on a cheap box. Your identity and your data don’t change, because they were never the hub’s to hold.

Six: proof you can actually leave

Every platform says “you can export your data.” What they mean is they’ll hand you a box of stuff in a format only they fully understand, and wish you luck. “Exit” on those terms is a courtesy they can withdraw. On xNet, exit isn’t a feature that was added — it’s a property of the format itself, and you can prove it.

Because every step we’ve described — the canonical fingerprint, the deterministic signature, the three-line merge — is specified down to the byte, the protocol ships with golden vectors: frozen test cases that pin down the exact expected output. A given edit must produce this exact fingerprint and this exact signature, full stop.

Open the panel — a fact you can re-derive
conformance/vectors/change/0001-create-page.json
{
  "description": "first change for a Page node",
  "expected": {
    "hash": "cid:blake3:76fdfa20…626cb980",
    "signatureBase64": "UcVsz+shSANm…5yE2AQ=="
  }
}
This exact case passes against xNet's implementations in TypeScript, Rust, Swift, and Python. The format isn't a vendor's blob you have to trust — it's a specification anyone can re-implement from the test vectors.

That’s why there are already working versions of the core in four languages, and why you could write a fifth. It’s why you can fork the whole project and your data still reads. The right to leave isn’t a promise printed on the box; it’s a consequence of the box being made of glass. That is what the weavers were never given: a machine whose workings were open enough that no owner could trap them inside it.

How this compares to the other escape routes

xNet isn’t the only project trying to pry the web back open, and it’s worth being clear about how it differs — because mostly, it’s solving a different problem. The well-known decentralised networks (Mastodon, Bluesky, Nostr) are about one shared social graph: how servers pass public posts around. xNet is about storage for everything you make — notes, tasks, a CRM, a wiki — with the master copy on your own device. They can happily coexist; they just answer different questions.

System Your identity is… Your data lives… Works fully offline?
Big-tech cloud an account they issue on their servers No
Mastodon (ActivityPub) tied to your home server on your home server No
Bluesky (AT Protocol) a portable DID on a host you pick No
Nostr a raw key pair on relays you pick No
xNet a did:key you mint on your device first Yes

The family resemblance is real — Nostr’s raw key pair is a cousin of xNet’s did:key, and Bluesky’s portable DID shares the instinct. The thing that’s distinctly xNet’s is the corner you’ve been standing in this whole essay: the real copy is on the edge device, in your hand, and the network is a convenience layered on top — not the place your life is kept.

What this isn’t

Opening the hood only counts if you’re honest about the parts that aren’t shiny. Four things this post is not claiming.

  • It isn’t a magic privacy box — the hub can see plaintext on the default path.

    The unencrypted path relays property values the hub can read. The encrypted, per-recipient envelope path exists (X25519-wrapped keys) and makes it blind — but it is not yet the universal default. We won’t call the whole thing end-to-end encrypted, because today it isn’t.

  • The kernel isn’t a CRDT in the Automerge sense.

    It’s a signed, hash-chained, last-write-wins change log. Rich-text documents layer Yjs (a real CRDT) on top, optionally. LWW is portable, deterministic, and trivially auditable across languages — at the cost of the fine-grained text-merge magic a sequence CRDT gives you. That’s a deliberate trade, not an accident.

  • It isn’t trying to beat Mastodon, Bluesky, or Nostr at federation.

    Those are server-to-server, online-first systems for one shared social graph. xNet is local-first storage for everything you make — notes, tasks, a CRM, a wiki — with the master copy on your device. Different problem; they can coexist.

  • Owning the format doesn’t make leaving effortless in real life.

    We can only make the part we control cheap to leave: your identity is a portable did:key, your history is an open log, the app works offline. Your friends, your habits, and your muscle memory are still switching costs we can’t wave away. The format just makes sure the door is never locked.

The loom you can read

Come back to the weavers. The histories now agree that they weren’t wrong about machines and right about nothing — they were making a precise argument about power. A tool that the people using it can understand, own, and walk away from leaves them free. A tool that’s sealed, rented, and built to make them dependent leaves them owned, however shiny it looks on the showroom floor. The weavers lost that argument to the factory, and we’ve been living in the factory’s logic online ever since — feeding sealed looms our attention and our words and calling the cloth they sell back to us “our” feed.

None of what you just read is exotic. Hashes, signatures, a tiny merge rule, a key for a name, a file on your own disk. It’s plain engineering, chosen specifically so that the machine can be opened and understood — by a developer reading the source, and by you, reading this. That openness is the politics. You can’t be quietly trapped inside a loom whose every thread you’re allowed to follow.

So follow them. Use the app — it’s free, offline, and private — and your “Buy milk” really will take the path you just traced. Build something of your own on the open protocol, or read the commitments that say, in writing and with the receipts to back them, what this loom will and won’t do. The weavers only ever wanted a machine that was theirs to understand and theirs to keep. Two centuries late, here is one.


Sources

Code excerpts are trimmed and lightly simplified for reading; the file paths point at the real, unabridged source. The Luddite history is used as argument, not decoration — the claim that they were pro-tool and anti-enclosure is the mainstream historical reading, not a rhetorical flourish. All artwork here is original.