I’ve spent the last year working on local-first apps, most recently with Muni Town. For me, ‘local-first’ isn’t just a technical architecture — it’s a political and social stance. It’s about shifting control: from remote servers and top-down central authorities deciding how data, workflows, and communities operate, to individuals and communities reclaiming that control and gaining autonomy. Seen this way, privacy and consent aren’t add-ons — they’re foundational, just as critical as sync or data locality.
Ink & Switch’s Keyhive project is a capabilities-based system for CRDT authorisation and sync that opens up real possibilities for privacy-preserving local-first apps. Libraries like Automerge, Yjs, and Loro have already made it viable to build real-time collaborative apps without relying on a centralised authority to manage consistency. Yet in practice, we still lean on central servers for sync — with all the privacy implications that entails.
Encrypting CRDT operations end-to-end sounds like an easy fix, but in practice it’s tricky. Most CRDT implementations rely on batching many fine-grained operations — often keystrokes — into compressed runs to reduce metadata overhead. Encrypting every individual operation separately breaks that optimisation, making CRDTs too inefficient to use. On top of that, group collaboration introduces the full complexity of Secure group messaging, where typical encryption schemes (like Signal’s) don’t translate cleanly due to CRDTs’ inherent concurrency and branching structure.
And even then, encryption only solves the read side of the problem. Keyhive goes further, enabling distributed capability-based authorisation. It models access control roles — read, write, admin — through chains of signed delegations. It also introduces Sedimentree, a mechanism for probabilistically compressing and encrypting CRDT updates based on DAG depth (as a proxy for concurrent update likelihood).
At Muni Town, we’re planning to use Keyhive for access control in Roomy, so I’ve been digging deep into how it works. The part I’m most interested in is BeeKEM, their proposed Key Encapsulation Mechanism. It formalises how keys are exchanged and managed within this decentralised, capability-based system. I wrote a research report for uni last year on almost exactly this topic, which made me especially keen to put that knowledge into use here. While the Keyhive team haven’t published a paper as yet, there is a Lab Notes article, various bits of documentation, and a Rust implementation. To try to learn-by-teaching, I’ll be doing my best in this post to explain how BeeKEM works.
Secure messaging basics
Since Signal protocol (aka the Double Ratchet algorithm) set a new standard for end-to-end encrypted messaging, there has been a lot of academic focus on abstracting and formalising the key components and pushing them further. Signal protocol is fundamentally designed for two-party secure messaging (2SM), meaning that every message sent using the protocol is encrypted with a key that is shared only with the recipient, which (through very clever use of Diffie-Hellman key exchange and keyed hash functions) they both independently derived for that message. It’s possible to use the Signal protocol for group messaging by applying the two-party protocol for every pair of group members. The catch is that you have to re-encrypt your messages for every group member, and everyone has to derive keys for everyone else, so in general for n group members there is n^2 complexity, which gets infeasible fast.
A complete graph, representing the number of pairwise protocol instantiations for a Signal group of 7 people. Source
Academics and industry people have since dedicated a lot of attention to finding a solid way to achieve efficient secure group messaging (SGM). A key requirement for this was to make it possible for encrypted message broadcast - that is, for each group message we send, we can broadcast one encrypted message for the entire group to decrypt - without losing the key security properties of Signal protocol. Thus far the most widely adopted approach has been Sender Keys, used by WhatsApp and Signal. Essentially every group member sends every other group member, via pairwise 2SM, a symmetric key that it will use to encrypt messages. To receive messages, users keep a list of these keys, one for each other member. These keys can then be ‘ratcheted’ with a keyed hash function to derive new keys for each message - once the old keys are deleted, this provides Forward secrecy, meaning that old encrypted messages are protected from a later key compromise. This simple approach makes application-level stuff (sending messages) fairly efficient - it’s just the group-level (key management) stuff, such as adding and removing members, which retains the performance limitations of pairwise groups.
TreeKEM
... continue reading