Your account is not a device. It is a set of them: each holding a complete copy of your messages, your Notebook and your agent's memory, and each equally entitled to change any of it. GroupThink is the machinery that keeps that set thinking as one: finding what drifted apart, agreeing whose edit survived, taking a new device in and putting an old one out, and deciding which device acts when only one may. No server holds the authoritative version, so no server can settle an argument between them, and every question that server would have answered, the devices must answer for themselves. Those questions are what this series is about.
Prerequisites: none. This is the top of a series. The transport underneath has its own series: The Relay covers what a sealed envelope guarantees, and Contact Discovery covers how two people come to share a key at all. If you have read Agent, this is the pillar it kept pointing at.
Most apps that run on more than one screen quietly nominate one of them as real. The phone holds your data and the tablet is a window onto it; or the account lives in a datacentre and every device you own is a view. Either way there is one authoritative copy, and the other surfaces are projections of it.
Pixie makes the opposite choice, and everything else in this series follows from it. Every device on your account is a full replica: a complete, equal copy. Your phone, your tablet and your desktop each hold the whole store (messages, contacts, Notebook, memory log, plans), and each may write to any of it, offline, without asking permission from another device or from a server. They are siblings: peers of equal standing, none of which is the original.
The fork is easiest to see at the moment an app grows a desktop version, because that is where the cheap answer is most tempting. A companion is a second surface that reads through the phone or through a cloud account holding the real data.
A companion has no state of its own to be wrong about. In exchange, it takes on a hard dependency. Either the phone must be awake and reachable, or a server must hold your plaintext (your data in readable, unencrypted form) so both ends can read it. Pixie allows neither, so the companion design is not so much rejected as unavailable.
Concretely, then: a Mac running Pixie registers its own row in the identity service, exactly like a phone does. It brings its own device keypair: a matched public and private key, where the public half is shared and the private half never leaves the device. The pair has two halves: an Ed25519 envelope key, built on a classical elliptic-curve signature scheme, and its ML-DSA-65 counterpart, a lattice-based scheme designed to stay secure even against a quantum computer.
A call to GET /v1/sync/siblings returns the account's device public keys. The server decides which account from the caller's login session, not from anything in the request, so a caller can only ever list its own fleet: the set of devices on its own account. The Mac holds a complete replica. Nothing about it is a projection of the phone, which is what The Homebase means when it calls the desktop a full peer rather than a companion.
Say what that buys before counting what it costs. Every device works at full capability with no network and no sibling awake. Losing one loses nothing. No machine you own is demoted to a terminal for another, and no server is ever in a position to be asked which version is right, because it does not have a version.
The bill arrives immediately, and it has a single line: there is no distinguished copy. With three devices you have three complete replicas and no tiebreaker. "Primary" does exist as a word here, but it means a rank in an election (§8), not a data authority. Every convergence problem in this series is the same bill, itemised.
Your phone and your Mac are almost never on the same network, and neither has a route to the other. They talk through the relay (the server that forwards Pixie traffic), and it carries every byte of your fleet's coordination. That would be disqualifying if the relay could read any of it.
It cannot. Traffic between your own devices reuses the machinery built for messaging a contact, wholesale, with one substitution: the key. Every granted device holds a 32-byte account root secret. The founding device mints it while it is still the account's only device, and every later device receives it through the sign-in approval.
From that secret, InnerMAC.siblingChannelKey derives a sibling channel key shared by exactly your devices. The derivation uses HKDF: a standard recipe for grinding one secret into several unrelated-looking keys. Each derived key is pinned to a fixed info string, a label baked in so one derived key can never be confused with another. The sibling channel key then plugs into the same slots a contact's handshake-derived key plugs into:
sync_delta frame collapses to silent, the same class that reactions and consent updates ride.The frames carry a free-form kind label and an opaque, encrypted body (state_digest, reconcile_req, reconcile_resp, gc_propose, heartbeat), none of which the relay parses.
Now the part that makes the channel self-gated: the channel polices its own membership, with no separate rule list to enforce. Every frame carries a MAC: a message authentication code, the short tag that proves the frame was written by a holder of a particular key and altered by nobody since. On receipt, the device must decide which key should verify that tag. It decides by reading the verified sender certificate: ChannelKeys.resolve returns the sibling key if and only if the certificate's user_id_hash (the hashed fingerprint that stands in for an account) equals our own, and otherwise looks up the contact bound to that hash. The syncDelta dispatch then re-checks senderUserIDHash == myUserIDHash before applying anything.
Follow what an attacker from another account would need. To reach the subject at all, they would need the sibling key. To carry a certificate claiming our account, they would need a credential the credential service will not mint for them. To pass the MAC, they would need the sibling key again, because the account hash they claim is the very field that selects the key their message must verify under. The check is not a policy test on an attacker-controlled field. The field chooses its own judge, and if no key resolves, the envelope is rejected.
Here is the constraint that makes the rest of this series interesting, stated as what we gave up.
A conventional multi-device system converges by having a trusted server hold the authoritative copy. That one decision quietly answers five separate questions. The server knows what each device last received, so it can say what changed.
It serializes writes, so concurrent edits arrive in an order and conflicts have a winner by construction. It is told about deletions and remembers them, so a delete stays deleted. It holds the account record, so it knows who is in the account. And it can hand out a lease, so exactly one device runs the background job.
We do not have that server, and not by oversight. The standing invariant (the rule that never bends) is that account identity, keys, and plaintext are never concentrated in one place. Even if we wanted the relay to arbitrate, it could not: it does not know that two envelopes on two rotating subjects belong to the same person, and cannot read either.
So the five questions come back, unanswered, and the devices have to answer them for themselves. That is the table of contents.
| The question a sync server would answer | Where the devices answer it |
|---|---|
| What diverged? | Staying in Sync → Reconciling by Digest |
| Whose edit wins? | Clocks You Cannot Trust |
| Did that delete really happen? | Deleting Things That Stay Deleted → The Convergence Barrier and the Cloud Anchor |
| Who is in the account? | Joining and Leaving → Absorbing a New Device, Revoking a Device |
| Who is acting? | Who Acts → The Driver's Seat |
Two devices that have been apart need to find their difference. The obvious method is to exchange inventories: you list what you have, I list what I have, and we compare the lists. Count the cost: a modest account holds tens of thousands of message rows, the check runs every time a device wakes or reconnects, and the overwhelmingly common answer is nothing changed. A transfer that grows with everything you own (O(n) in the row count, meaning the cost climbs in step with the number of rows) just to learn that nothing changed is the wrong shape.
So the cheap question is made answerable in constant space: the same few bytes no matter how much data sits behind them. State is partitioned into namespaces (settings, messages, memory, notebook, saga, contacts, plans, peerDevices, fileIndex, ktIdentityLedger, blocks, posts, agentConversations), and each namespace is hashed into one 32-byte Merkle root: the top of a tree of hashes in which changing any row anywhere underneath changes the root.
A whole StateDigest is a version byte plus one root per namespace: a few hundred bytes, and a
fixed size however much you own. Equal roots mean the devices are provably in sync with no data
transferred; an unequal root says which namespace to descend into, and only that one.
The load-bearing part is not the tree; it is the leaf encoding contract: the exact bytes each row is turned into before it is hashed. Those bytes must be identical on every device and every app version. So the leaf commits only to content that is supposed to converge, and it excludes anything inherently per-device: when this device first saw a message, local row identifiers, a contact's link into the system address book, the outbox.
Dates are hashed at whole-second granularity, so a fractional-second rounding drift between devices cannot manufacture a difference. Optional fields carry a presence byte, so "absent" and "empty" never collide. Get one field wrong and two devices are permanently, silently "divergent", reconciling forever over a difference that is not real.
Once the differing namespace is pinned down, the transfer is a pull. A reconcile_req carries range fingerprints (one hash covering a contiguous block of the requester's rows), and the reconcile_resp carries back the rows that differ. Both ride as sync_delta sub-kinds, so reconciliation needed no new wire type and inherits §2's sealing and self-gating for free. The pull is one-directional, but each device runs its own pull independently, which is how a pair converges with no leader. Staying in Sync tells that story, and Reconciling by Digest covers the mechanics.
You retitle a note on your phone at 9:02 and, offline, retitle it on your Mac at 9:03. Both edits are real. One has to lose.
The intuitive rule is newest-wins on a timestamp, and it is wrong in a way that is hard to see. Device clocks disagree. A phone running two minutes fast wins every contested edit against every other device, permanently, and there is no error to notice: the losing edit is not rejected, it is overwritten and gone.
The replacement is a version vector: a small table mapping each device to a counter that only counts up. A device bumps its own counter when it edits a row. Comparing two vectors then answers a question about causality, not time: did one edit provably come after the other had been seen, or are the two concurrent, made independently, neither aware of the other? No wall clock is involved anywhere. Merging two vectors takes the larger counter in each slot, so vectors converge alongside the values they describe.
The gain is not that conflicts stop happening. It is that a genuine conflict becomes a detectable state rather than an invisible one. At that point the tie can be broken deterministically (the row whose encoded bytes sort higher wins, the byte-string equivalent of alphabetical order), so both devices independently choose the same winner without a round trip.
Two details matter more than they look. The vector is not part of the content leaf, so this bookkeeping can never disturb the digest of §4. And only four namespaces track causality at all: notebook, saga, plans, contacts; the rest get the weakest rule that still converges, which Clocks You Cannot Trust works through case by case.
Deletion is the hard operation in a replica set, and the reason is almost philosophical: absence carries no evidence. Every other change leaves a trace you can ship. A delete leaves a gap, and a gap is indistinguishable from never having had the thing.
Play it out. You delete a note on your phone; your iPad was offline and never heard. On the next reconcile the iPad notices it holds a row the phone lacks (exactly the situation the protocol exists to repair) and helpfully ships it back. The deletion undoes itself, and the more reliable your sync is, the more reliably it undoes itself.
The fix is to make the deletion a thing rather than the absence of one. A tombstone records the row's key and the version vector at the moment of deletion, and travels by the same path as an ordinary row, so it needs no new machinery. The receiver deletes only when the tombstone causally dominates its own copy (when the tombstone's version vector shows the delete came after every edit the receiver knows about), so a concurrent live edit survives. The row's version record outlives the row it describes.
One consequence is worth noticing. A deleted row no longer appears in any live leaf, so no range of the tree can point at its tombstone. The responder therefore has to ship its whole tombstone set rather than only the part covering a mismatched range, worked through in Deleting Things That Stay Deleted.
That leaves the bind the series treats as its hardest problem. Tombstones cannot be kept forever, because they grow without limit. And they cannot be dropped on a timer, because a device that was offline past the deadline will ship the deleted rows straight back.
The answer is to collect garbage on evidence. A barrier round (gc_propose / gc_ack / gc_commit), stamped with a nonce (a fresh random number that keeps old acknowledgements from being replayed), has every sibling confirm it stands at the agreed digest. On top of that, a generation counter that only moves forward, anchored in iCloud Drive, catches what a barrier cannot: a device restored from an old backup, re-entering with pre-deletion state and no memory of the round. The Convergence Barrier and the Cloud Anchor is the deepest article here.
Everything above presumes the set of devices is known: the one question the devices genuinely cannot answer among themselves. A device that has never heard of a third sibling has no way to distinguish "there is no third device" from "the third device is asleep." Silence is not evidence.
So the roster (the list of devices on the account) is server-authoritative. The identity service holds the account's device rows, derives the account from the caller's session, and filters revoked devices out. The split is the interesting part: membership is server-authoritative while the content of the account is device-authoritative. The server decides who is in the set and can read nothing the set holds.
The two directions resolve in opposite ways. Absorbing a device inverts the usual flow. Instead of presenting a password to a server, the new device proves control of the phone number with an ownership certificate. It then resolves its own account's discovery inbox (the drop point where sealed requests addressed to the account wait) while holding no session at all, and pushes a sealed sign-in request that the existing devices approve.
Then it takes a snapshot. A sibling exports its store, seals (encrypts) it, uploads it as a blob (an opaque file the storage service holds without being able to read), and offers the decryption key inside a sealed frame. The joiner fetches the blob, verifies its SHA-256 (a cryptographic fingerprint proving the bytes arrived intact), decrypts, and imports wholesale. It re-asks every five seconds, up to seven times, because the relay has no mailbox (nothing waits there for a device that is offline) and the sibling may be asleep. Why a snapshot and not a replay of history is Absorbing a New Device.
Expelling one is where the series states a debt. Removal marks the roster row revoked and rotates the account's shared discovery identity away from the removed machine. That rotation is the one part that needs nothing from the removed device, and the one part that takes a live capability away from it. What the machine does with the news is still up to it: it discovers the revocation on its next self-status poll, logs out, and re-onboards with a fresh key. Revoking a Device is deliberate about which halves are which. Joining and Leaving frames both directions.
"All devices are equal" is the right rule for messages and the wrong rule for autonomous work.
Send a message from whichever device is in your hand; it fans out and everything converges. But run the nightly note-taking pass on your phone and your iPad and you get two sets of notes for one day.
Join a conversation between agents from two of your devices and your agent shows up twice to a discussion among three people. Autonomous work must run exactly once, so the fleet has to elect one device to do it, and there is no election service, since adding one would mean a server tracking your devices and which of them are awake.
The election is therefore built from what already exists. Rank is chronological by default: the earliest-registered device is primary. That is a total order (a ranking with no ties) needing no negotiation, because the roster already records who registered when. A user-set device order overrides it, and that order itself syncs, so every sibling elects the same way. The claim to the seat rides the sealed heartbeat (the small periodic here-I-am frame siblings already exchange) as two bytes: do I hold the seat, and do I want you to answer now. So there is no presence service either: no server tracking which of your devices is awake.
The failure modes it is designed against are named up front: orphaned control, where nobody drives because the taker crashed mid-handoff, and sustained overlap, where two devices drive and everything happens twice.
The confirmed two-phase handoff avoids both: the seat is never left unheld, and driving never overlaps. A holder that sees a higher-priority device come online quiesces: it stops working and flushes its context, but keeps asserting the token. A lame duck. It releases only once it has seen the new driver assert the seat. Both hold the token for a beat; only one is ever driving. Who Acts and The Driver's Seat take it from here.
From a distance, sending to your iPad and sending to a friend look identical: same relay, same sealed envelope, same rotating subject. They are not the same path, and the difference is exactly two things: which key seals the frame, and which payload it carries. To a friend, a message under the channel key their handshake produced. To yourself, a sync_delta under the account-derived sibling key, self-gated on arrival.
That separation is load-bearing for a feature in the Agent series. Typing @pixie in a conversation summons your own agent into the thread, and the person you are talking to never learns it happened. The summon and the reply are stored as ordinary message rows flagged as agent-tag rows, never enqueued for delivery. They still appear on your iPad, because the messages namespace of the reconcile carries them, like any other row.
Work out why the friend can never receive one. There are only two ways it could reach them. Either the peer-send path emits it, and that path has one chokepoint, ComposeService.assertPeerSendable, which throws an error on a summon-prefixed body before anything is sealed. Or the reconcile ships it, and a reconcile response is only ever produced in answer to a reconcile_req, which arrives only inside a sync_delta, which is only applied when the verified sender certificate carries our own account hash.
A friend's device cannot produce that frame; it does not hold the key. The sync path structurally cannot address a non-sibling, and the peer path structurally refuses this content. Two independent mechanisms, neither of them a rule the code is trusted to remember.
Read top-down for the argument, bottom-up for the engineering. Everything below assumes Staying in Sync; if you read one mechanism, read The Convergence Barrier and the Cloud Anchor.
Three things this series does not cover, because a sibling series owns each. How an envelope is sealed, what the relay can observe, and how group cryptography works: The Relay. How two people find each other without the server learning who knows whom: Contact Discovery. What the agent riding on this coordination actually does: Agent.
A closing word on the name, because it labels a paradigm rather than a feature. Nothing in this series is something a device does by itself. A digest means nothing until another device compares it. A version vector is one device's account of what it has already heard from the others.
A seat exists only because the rest of the fleet declines to take it. Even a deletion is not finished until every sibling has agreed to remember that it happened. GroupThink is that standing agreement between your devices, and its measure of success is that you never once notice it is being maintained.
Next: Staying in Sync.