This Month in Automerge: November '26
A quick note on naming: starting with this edition, each issue is named for the month it comes out, not the month it covers. This one covers October, picking up right where the September edition left off, so nothing was skipped.
As always, the community hangs out in the Automerge Discord. Come say hi, and let us know if you’d like something included in next month’s edition!
✨ Highlights
- Keyhive had a big month: a streamlined authority graph (Keyline), causal encryption, and a canonical codec all landed in Keyhive 0.6.
- automerge-repo 3.0, rebuilt on Subduction, is now out as an alpha.
📺 Community Call
On October 8, Brooke Zelenka and Alex Good hosted our Q4 community call, and the recording is up on YouTube.
Want to join the next one? Follow the Automerge calendar on Luma to hear when it’s scheduled.
Automerge
Masking Changes by Author
In September, Automerge learned to record an author for each change. In October, Fintan Halpenny built the next step on top of that: hiding an author’s changes from your view of a document (#1581). It’s on main now and will ship in the next release.
import * as Automerge from "@automerge/automerge" // Hide everything `author` wrote after `heads`doc = Automerge.maskAuthor(doc, author, heads) Automerge.isAuthorMasked(doc, author) // true // ...and bring it backdoc = Automerge.revealAuthor(doc, author)The point at which the author is cut off is called a write frontier. Changes from that author up to the frontier stay visible, and anything after it is hidden. Masking doesn’t delete anything or move the document’s heads. It only changes what your replica shows, so you can undo it with revealAuthor.
We built this for Keyhive revocation. When Keyhive revokes someone’s access to a document, Automerge can stop showing the changes they made after that point, even if those changes still reach you through sync.
We think masking is useful well beyond access control. For example, you could hide edits from a misbehaving bot or AI agent, review someone’s changes before accepting them, or see what a document looks like without one collaborator’s contributions. If you have other ideas, we’d love to hear them!
Alex Good also made masking work when you only have part of the write frontier (#1595). Your replica no longer has to wait for every head before it can show the changes you do have.
Fintan made this fast too. Masked reads use Automerge’s indexes instead of scanning the whole document, and loading skips the reindex step when the mask hides nothing. Property tests check that patches and hydrated documents agree under every mask.
Fixes & Polish
- Aran Donohue fixed
list_rangeon text, which returned the start and end of each mark as if they were characters (#1608). For"hello world"with one mark, you now get eleven characters instead of thirteen items. Aran also added CI coverage for theutf16-indexingfeature (#1630). - Thanks to first-time contributor Wahaj18, looking up a cursor against the wrong object now returns
InvalidCursorinstead of panicking (#1597). - Fintan Halpenny migrated another benchmark (
load_actors) from Criterion into the benchmark battery.
📦 automerge-repo
Darcy Parker kept up the steady stream of fixes:
- Sync state is now saved per document, and removing
DocHandlelisteners now works correctly (#773). react-hooksandsolid-primitivesno longer bundle their dependencies (#758).- The minimum versions of
cbor-xandwswere raised to pick up upstream fixes (#756). - Importing an older copy of a document under the same ID can no longer delete newer stored changes during compaction (#774).
automerge-repo v3.0 (Alpha)
Alex Currie-Clark spent much of October rebuilding automerge-repo on top of Subduction. Instead of plugging network and storage adapters straight into the Repo, automerge-repo 3.0 talks to a Sedimentree backend that owns them, and the first one is built on Subduction. The work went out early in the month as v3.0.0-experimental.1 and .2, on npm under the experimental tag, and continued on its branch through the rest of October. In early November it graduated to a proper alpha: v3.0.0-alpha.1 is on npm under the next tag.
What’s new:
- A simpler core. The old scheduler is gone. Each document now owns its own state, and the
Repojust maps document IDs to documents. - New APIs.
Repo.query()returns an observable you can subscribe to, andDocHandle.hasHeadschecks whether a document has reached some heads.DocHandle.changeAtnow returns aPromiseof the new heads. - 32-byte document IDs. Existing 16-byte IDs still work in URLs.
- Faster loading. Each document loads with a single storage read, and many documents load in parallel. New benchmarks cover large imports, long text histories, and documents with many authors.
- Clearer failures. If a write fails, the repo stops and reports it, instead of retrying silently. Edits are now stored as loose commits and compacted later.
This is an alpha: it’s not ready for production yet, and the API may still change. If you try it, we’d love to hear how it goes in the Discord!
🗝️🐝 Keyhive
Keyhive is our access control and encryption layer for Automerge and other CRDTs. This was a big month for Keyhive: a lot of work that’s been happening behind the scenes finally landed!
In short: a new authority graph (Keyline), sturdier group keys, causal encryption, a canonical custom codec (heading for a stable format), no more “World Public” user, and Keyhive 0.6 running in automerge-repo-keyhive. Everything below ships in @keyhive/keyhive v0.6.0 and keyhive_core v0.10.0.
Keyline
Keyline is Keyhive’s new authority graph. It used to live directly in keyhive_core, but we split it out into its own crate for symmetry with BeeKEM (#230), and Keyhive’s membership now runs on it (#288).
As before, it is a set of signed certificates over keys: delegations that give a key some power over a subject, and revocations that withdraw a delegation. There’s a detailed design, including the alternatives we rejected and the edge cases. Unlike the prior authority graph, capabilities are now bound late (when they’re used) rather than early (at delegation time). That idea was inspired by UCAN, and it’s a natural fit for a causally consistent system.
Keyline also replaces our old answer to the “dueling admins” problem. Previously, when two admins revoked each other concurrently, Keyhive broke the tie with a deterministic but arbitrary order (seniority, then causal depth, then hash), which was hard to predict. Keyline’s rules are simpler and explicit: you can never revoke the branch of the graph you’re standing on, and when peers revoke each other, both revocations stand. Below the root role, you recover by rotating the role. A duel inside the root role can’t be undone, so keep that role small.
One of the interesting things about Keyline is that it has clear semantics and can be implemented in terms of SQL, Datalog, or DBSP. The first version is a fully in-memory implementation, but we plan to use databases for things like sync servers in the future. Keyline is also no_std and builds for Wasm. Its interface is async, so a backend can use a database, and signing can happen behind WebCrypto or a hardware key.
Expect an Ink & Switch lab note all about Keyline in the near future!
CGKA Hardening
John Mumm continued hardening BeeKEM, Keyhive’s group key agreement, building on the work from September. For apps, that means membership changes made concurrently by different people now merge cleanly (#252, #256, #305, #306), every key operation is checked against the certificates that authorize it (#295, #302, #303), and newly invited members can decrypt a document right away (#262). BeeKEM also stays consistent when signing fails or a secret is wrong, and refuses to record secrets it hasn’t verified (#263, #293).
Causal Encryption
John Mumm moved content encryption into its own crate, keyhive_content (#313), and exposed causal encryption to both Rust and JS clients (#317).
Every encrypted block carries the keys of its direct causal ancestors. If you can open one point in a document’s history, you can walk back from there and open everything before it, so a new member can read the full history without anyone having to hand over a pile of old keys. In JS, that’s tryEncrypt (passing the predecessors) and tryCausalDecrypt. The design doc covers the trade-offs, including why this gives post-compromise security but not forward secrecy.
The format got stricter along the way. Encrypted content is now chunked with a checked nonce (#297), every block body is an envelope (#322), and an ancestor key must actually be able to decrypt the content it’s listed with (#323).
Canonical Encoding
Brooke Zelenka has been finalizing Keyhive’s custom codec, with two goals: dropping bincode and serde (#299, #310, #311), and reaching a stable byte format we can commit to, since Keyhive data is signed, hashed, synced, and stored for the long term.
Getting the bytes right matters for security too. keyhive_codec guarantees that each value has exactly one valid encoding, so a peer can’t send the same certificate under two different hashes and slip past a revocation that names one of them. Every key derivation and every signed piece of application data also gets its own context now (#296, #309), so a key or signature made for one purpose can’t be passed off as another.
No More “World Public” User
The “World Public” user was always a bit of a hack that we knew we wanted to get rid of. Public documents used to be modeled as a well-known member whose secret any replica could hold. We removed it (#284) for two reasons:
- It was special-cased throughout the code. BeeKEM,
keyhive_core, and the JS API all had to know about it, and the new approach doesn’t need any of that. - It was a denial-of-service vector. Anyone could act as the “World Public” user and ask a sync server for every public document it held.
Public documents are now handled one layer up, with ordinary read and relay capabilities: instead of a global “world” key, each app ships its own keys for the content it treats as public.
Here’s the pattern for an “app-public” document. The app creates two principals and ships one key for each of them, so anyone running the app can read the document:
Anyone with read access to the document can set this up, not just its owner. They give the reader read access, and the reader passes relay access on to the fetcher. The app ships the reader’s X25519 secret key, so anyone with the app can decrypt as the reader, but not its signing key, so nobody can use it to change the document or its membership. It also ships the fetcher’s Ed25519 signing key, so anyone with the app can sign in to the sync server as the fetcher and pull ciphertext, which on its own they can’t read. Put the two together and you get read-only access for that app’s users.
These keys aren’t really secret: anyone who downloads the app can extract them. What you get is read-only access that’s opt-in and scoped to one app. Unlike the old “World Public” user, the fetcher can only pull the documents that were made public to that app, and sync servers still only ever see ciphertext.
Today this is set up by hand, but we plan to add an API for it.
automerge-repo-keyhive
automerge-repo-keyhive v0.8.0-alpha.1 (on the next npm tag) moves onto @keyhive/keyhive 0.6.0, thanks to John Mumm. Its hand-rolled encryption is gone, replaced by Keyhive’s causal encryption. Earlier in the month, v0.7.0-alpha.1 removed the old “nudge edit” machinery.
This is a breaking change: with the “World Public” user gone, automerge-repo-keyhive no longer has setPublicAccess or getPublicAccess. Public access is now up to your application (see the app-public pattern above). As always with automerge-repo-keyhive, pin an exact version, since the latest npm tag still points at a much older alpha.
🏔️ Subduction
A new batch of Subduction releases is out, including subduction_core v0.20.0, @automerge/subduction v0.26.0, and @automerge/automerge-subduction v0.21.0. Each package is now released and tagged on its own (#318), which is why the version numbers no longer move together.
- Alex Good fixed two Sedimentree sync bugs. Minimization now keeps fragment boundaries (#325), and boundaries are no longer wrongly advertised as held (#326). That second bug could make a peer skip sending a commit because it thought the other side already had it.
subduction_keyhivenow trackskeyhive_corev0.7.0, thanks to John Mumm (#323). It doesn’t include the later Keyhive changes above yet.- The Wasm storage API gained
get_head(id), and storage errors now carry the backend’s original error as theircause(#317). - Later in the month, Alex Good made relays wait for upstream fetches before answering, so downstream peers don’t wrongly conclude that a document is unavailable (#334). Metadata that exceeds the wire format’s limits now returns an error instead of panicking (#348).
🌱 Around the Ecosystem
Automerge in Go
Bryan Frimin at Probo, an open-source compliance platform, wrote a pure-Go implementation of Automerge to add real-time collaboration to its document editor. The PR reports full V2 wire and state interoperability with the Rust and JS implementations, checked by a differential parity and fuzzing suite that runs against both. It also includes an automerge-repo protocol client and a ProseMirror bridge. A follow-up PR wired it into the editor, with the official @automerge/automerge-repo client in the browser talking to a Go gateway on the server.
A Repo for Python
automerge-py has an async automerge.repo API (in its 0.2.0.dev pre-releases) that interoperates with the JavaScript automerge-repo. This month Alex Good added authors and commit metadata, ephemeral messages, read-only sync, and per-peer share policies. These are on main and haven’t been released yet.
Pushwork
Pushwork syncs a directory through Automerge, a bit like Git without the merge conflicts. It’s also becoming a good way to try Keyhive for real. chee rabbits’s work on Subduction and Keyhive support is on npm as [email protected], under the experimental tag:
# Only you can read or write this repo$ pushwork init --keyhiveINITIALIZED automerge:xtVWLeMLftzvzpw4yA2J4yomix8j # Save collaborators' contact cards under names, then list your contacts$ pushwork keyhive contacts add alice ./alice-card.jsonsaved alice (iN8BYtCAVQ+R1MHpcK7KMQPy4dqY3qmvZDLtBoWc1g8=) $ pushwork keyhive contacts add bob ./bob-card.jsonsaved bob (6vJtwT8fpanJsZ0TiXIikn9m7CJA29Nc3xxbJv7fNu8=) $ pushwork keyhive contacts lsalice iN8BYtCAVQ+R1MHpcK7KMQPy4dqY3qmvZDLtBoWc1g8=bob 6vJtwT8fpanJsZ0TiXIikn9m7CJA29Nc3xxbJv7fNu8= # Share with a contact, see who has access, or take access away$ pushwork keyhive access alice editalice can now edit $ pushwork keyhive accessyou adminkeyhive server relayalice edit $ pushwork keyhive access alice nonealice's access revokedBy default, the sync server only gets relay access: it stores and forwards your documents, but can’t read them. This release predates this month’s Keyhive changes, so it still runs on automerge-repo-keyhive 0.5 and 0.6 under the hood.
🎤 Presentations
Keyhive at J On The Beach
On October 30, Brooke Zelenka gave a talk at J On The Beach in Málaga: Keyhive: End-to-end Encrypted Applications. It asks what “Signal for documents” would look like, and walks through Keyhive’s answer: capabilities that travel inside the data, causal encryption that follows a document’s history, BeeKEM group keys without a central server, and sync servers that only ever see ciphertext. The video isn’t up yet, but you can flip through the slides (some of them are interactive):
📬 Get in Touch
Are you building something with Automerge? We’d love to hear about it! Drop us a line in the Discord. Knowing how Automerge is used in the wild is how we decide what to build next.
If Automerge is important to your product or organization, consider sponsoring its development: reach out to the team at [email protected].
Until next time 👋
— The Automerge Core Team