An Interactive Intro to CRDTs (2023)

2026-03-0319:2217933jakelazaroff.com

CRDTs don't have to be all academic papers and math jargon. Learn what CRDTs are and how they work through interactive visualizations and code samples.

Have you heard about CRDTs and wondered what they are? Maybe you’ve looked into them a bit, but ran into a wall of academic papers and math jargon? That was me before I started my Recurse Center The Recurse Center The Recurse Center is a self-directed, community-driven educational retreat for programmers in New York City. www.recurse.com/ batch. But I’ve spent the past month or so doing research and writing code, and it turns out that you can build a lot with just a few simple things!

In this series, we’ll learn what a CRDT is. Then we’ll write a primitive CRDT, compose it into more complex data structures, and finally use what we’ve learned to build a collaborative pixel art editor. All of this assumes no prior knowledge about CRDTs, and only a rudimentary knowledge of TypeScript.

To pique your curiosity, this is what we’ll end up with:

Draw by clicking and dragging with your mouse. Change the paint color by using the color input on the bottom left. You can draw on either canvas and your changes will show up on the other, as if they were collaborating on the same picture.

Clicking the network button prevents changes from reaching the other canvas (although they’ll sync up again when they come back “online”). The latency slider adds a delay before changes on one canvas show up on the other.

We’ll build that in the next post. First, we need to learn about CRDTs!

What is a CRDT?

Okay, let’s start from the top. CRDT stands for “Conflict-free Replicated Data Type”. That’s a long acronym, but the concept isn’t too complicated. It’s a kind of data structure that can be stored on different computers (peers). Each peer can update its own state instantly, without a network request to check with other peers. Peers may have different states at different points in time, but are guaranteed to eventually converge on a single agreed-upon state. That makes CRDTs great for building rich collaborative apps, like Google Docs and Figma — without requiring a central server to sync changes.

Broadly, there are two kinds of CRDTs: state-based and operation-based.1 State-based CRDTs transmit their full state between peers, and a new state is obtained by merging all the states together. Operation-based CRDTs transmit only the actions that users take, which can be used to calculate a new state.

That might make operation-based CRDTs sound way better. For example, if a user updates one item in a list, an operation-based CRDT can send a description of only that update, while a state-based CRDT has to send the whole list! The drawback is that operation-based CRDTs impose constraints on the communication channel: messages must be delivered exactly once, in causal order, to each peer.2

This post will exclusively focus on on state-based CRDTs. For brevity, I’ll just say “CRDTs” from here on out, but know that I’m referring specifically to state-based CRDTs.

I’ve been talking about what CRDTs do, but what is a CRDT? Let’s make it concrete: a CRDT is any data structure that implements this interface:3

interface CRDT<T, S> { value: T; state: S; merge(state: S): void;
}

That is to say, a CRDT contains at least three things:

  • A value, T. This is the part the rest of our program cares about. The entire point of the CRDT is to reliably sync the value between peers.
  • A state, S. This is the metadata needed for peers to agree on the same value. To update other peers, the whole state is serialized and sent to them.
  • A merge function. This is a function that takes some state (probably received from another peer) and merges it with the local state.

The merge function must satisfy three properties to ensure that all peers arrive at the same result (I’ll use the notation A ∨ B to indicate merging state A into state B):

  • Commutativity: states can be merged in any order; A ∨ B = B ∨ A. If Alice and Bob exchange states, they can each merge the other’s state into their own and arrive at the same result.
  • Associativity: when merging three (or more) states, it doesn’t matter which are merged first; (A ∨ B) ∨ C = A ∨ (B ∨ C). If Alice receives states from both Bob and Carol, she can merge them into her own state in any order and the result will be the same.4
  • Idempotence: merging a state with itself doesn’t change the state; A ∨ A = A. If Alice merges her own state with itself, the result will be the same state she started with.

Mathematically proving that a merge function has all these properties might sound hard. But luckily, we don’t have to do that! Instead, we can just combine CRDTs that already exist, leaning on the fact that someone has proven these things for us.

Speaking of CRDTs that already exist: let’s learn about one!

Last Write Wins Register

A register is a CRDT that holds a single value. There are a couple kinds of registers, but the simplest is the Last Write Wins Register (or LWW Register).

LWW Registers, as the name suggests, simply overwrite their current value with the last value written. They determine which write occurred last using timestamps, represented here by integers that increment whenever the value is updated.5 Here’s the algorithm:

  • If the received timestamp is less than the local timestamp, the register doesn’t change its state.
  • If the received timestamp is greater than the local timestamp, the register overwrites its local value with the received value. It also stores the received timestamp and some sort of identifier unique to the peer that last wrote the value (the peer ID).
  • Ties are broken by comparing the local peer ID to the peer ID in the received state.

Try it out with the playground below.

Did you get a sense for how LWW Registers work? Here are a couple specific scenarios to try:

  • Turn the network off, make a bunch of updates to bob, and then turn it back on. When you send updates from alice, they’ll be rejected until the timestamp exceeds bob’s timestamp.
  • Perform the same setup, but once you turn the network back on, send an update from bob to alice. Notice how the timestamps are now synced up and alice can write to bob again!
  • Increase the latency and send an update from both peers simultaneously. alice will accept bob’s update, but bob will reject alice’s. Since bob’s peer ID is greater, it breaks the timestamp tie.

Here’s the code for the LWW Register:

class LWWRegister<T> { readonly id: string; state: [peer: string, timestamp: number, value: T]; get value() { return this.state[2]; } constructor(id: string, state: [string, number, T]) { this.id = id; this.state = state; } set(value: T) { this.state = [this.id, this.state[1] + 1, value]; } merge(state: [peer: string, timestamp: number, value: T]) { const [remotePeer, remoteTimestamp] = state; const [localPeer, localTimestamp] = this.state; if (localTimestamp > remoteTimestamp) return; if (localTimestamp === remoteTimestamp && localPeer > remotePeer) return; this.state = state; }
}

Let’s see how this stacks up to the CRDT interface:

  • state is a tuple of the peer ID that last wrote to the register, the timestamp of the last write and the value stored in the register.
  • value is simply the last element of the state tuple.
  • merge is a method that implements the algorithm described above.

LWW Registers have one more method named set, which is called locally to set the register’s value. It also updates the local metadata, recording the local peer ID as the last writer and incrementing the local timestamp by one.

That’s it! Although it appears simple, the humble LWW Register is a powerful building block with which we can create actual applications.

Last Write Wins Map

Most programs involve more than one value,6 which means we’ll need a more complex CRDT than the LWW Register. The one we’ll learn about today is called the Last Write Wins Map (or LWW Map).

Let’s start by defining a couple types. First, our value type:

type Value<T> = { [key: string]: T;
};

If each individual map value holds type T, then the value of the entire LWW Map is a mapping of string keys to T values.

Here’s our state type:

type State<T> = { [key: string]: LWWRegister<T | null>["state"];
};

Do you see the trick? From our application’s perspective, the LWW Map just holds normal values — but it actually holds LWW Registers. When we look at the full state, each key’s state is the state of the LWW Register at that key.7

I want to pause on that for a moment, because it’s important. Composition lets us combine primitive CRDTs into more complex ones. When it’s time to merge, all the parent does is pass slices of incoming state to the appropriate child’s merge function. We can nest this process as many times as we want; each complex CRDT passing ever-smaller slices of state down to the next level, until we finally hit a primitive CRDT that performs the actual merge.

From this perspective, the LWW Map merge function is simple: iterate through each key and hand off the incoming state at that key to the corresponding LWW Register to merge. Try it out in the playground below:

It’s kind of difficult to trace what’s happening here, so let’s split up the state for each key. Note, though, that this is just a visualization aid; the full state is still being transmitted as a single unit.

Try increasing the latency and then updating different keys on each peer. You’ll see that each peer accepts the updated value with a higher timestamp, while rejecting the value with a lower timestamp.

The full LWW Map class is kinda beefy, so let’s go through each property one by one. Here’s the start of it:

class LWWMap<T> { readonly id = ""; #data = new Map<string, LWWRegister<T | null>>(); constructor(id: string, state: State<T>) { this.id = id; for (const [key, register] of Object.entries(state)) { this.#data.set(key, new LWWRegister(this.id, register)); } }
}

#data is a private property holding a map of the keys to LWW Register instances. To instantiate a LWW Map with preexisting state, we need to iterate through the state and instantiate each LWW Register.

Remember, CRDTs need three properties: value, state and merge. We’ll look at value first:

 get value() { const value: Value<T> = {}; for (const [key, register] of this.#data.entries()) { if (register.value !== null) value[key] = register.value; } return value; }

It’s a getter that iterates through the keys and gets each register’s value. As far as the rest of the app is concerned, it’s just normal map!

Now let’s look at state:

 get state() { const state: State<T> = {}; for (const [key, register] of this.#data.entries()) { if (register) state[key] = register.state; } return state; }

Similar to value, it’s a getter that builds up a map from each register’s state.

There’s a clear trend here: iterating through the keys in #data and handing things off to the register stored at that key. You’d think merge would work the same way, but it’s a little more involved:

 merge(state: State<T>) { for (const [key, remote] of Object.entries(state)) { const local = this.#data.get(key); if (local) local.merge(remote); else this.#data.set(key, new LWWRegister(this.id, remote)); } }

First, we iterate through the incoming state parameter rather than the local #data. That’s because if the incoming state is missing a key that #data has, we know that we don’t need to touch that key.8

For each key in the incoming state, we get the local register at that key. If we find one, the peer is updating an existing key that we already know about, so we call that register’s merge method with the incoming state at that key. Otherwise, the peer has added a new key to the map, so we instantiate a new LWW Register using the incoming state at that key.

In addition to the CRDT methods, we need to implement methods more commonly found on maps: set, get, delete and has.

Let’s start with set:

 set(key: string, value: T) { const register = this.#data.get(key); if (register) register.set(value); else this.#data.set(key, new LWWRegister(this.id, [this.id, 1, value])); }

Just like in the merge method, we’re either calling the register’s set to update an existing key, or instantiating a new LWW Register to add a new key. The initial state uses the local peer ID, a timestamp of 1 and the value passed to set.

get is even simpler:

 get(key: string) { return this.#data.get(key)?.value ?? undefined; }

Get the register from the local map, and return its value if it has one.

Why coalesce to undefined? Because each register holds T | null. And with the delete method, we’re ready to explain why:

 delete(key: string) { this.#data.get(key)?.set(null); }

Rather than fully removing the key from the map, we set the register value to null. The metadata is kept around so we can disambiguate deletions from states that simply don’t have a key yet. These are called tombstones — the ghosts of CRDTs past.

Consider what would happen if we really did delete the keys from the map, rather than leaving a tombstone. Here’s a playground where peers can add keys, but not delete them. Can you figure out how to get a peer to delete a key?

Turn off the network, add a key to alice’s map, then turn the network back on. Finally, make a change to bob’s map. Since alice sees that the incoming state from bob is missing that key, she removes it from her own state — even though bob never knew about that key in the first place. Whoops!

Here’s a playground with the correct behavior. You can also see what happens when a key is deleted.

Notice how we never remove deleted keys from the map. This is one drawback to CRDTs — we can only ever add information, not remove it. Although from the application’s perspective the key has been fully deleted, the underlying state still records that the key was once there. In technical terms, we say that CRDTs are monotonically increasing data structures.9

The final LWW Map method is has, which returns a boolean indicating whether the map contains a given key.

 has(key: string) { return this.#data.get(key)?.value !== null; }

There’s a special case here: if the map contains a register at the given key, but the register contains null, the map is considered to not contain the key.

For posterity, here’s the full LWW Map code:

class LWWMap<T> { readonly id: string; #data = new Map<string, LWWRegister<T | null>>(); constructor(id: string, state: State<T>) { this.id = id; for (const [key, register] of Object.entries(state)) { this.#data.set(key, new LWWRegister(this.id, register)); } } get value() { const value: Value<T> = {}; for (const [key, register] of this.#data.entries()) { if (register.value !== null) value[key] = register.value; } return value; } get state() { const state: State<T> = {}; for (const [key, register] of this.#data.entries()) { if (register) state[key] = register.state; } return state; } has(key: string) { return this.#data.get(key)?.value !== null; } get(key: string) { return this.#data.get(key)?.value; } set(key: string, value: T) { const register = this.#data.get(key); if (register) register.set(value); else this.#data.set(key, new LWWRegister(this.id, [this.id, 1, value])); } delete(key: string) { this.#data.get(key)?.set(null); } merge(state: State<T>) { for (const [key, remote] of Object.entries(state)) { const local = this.#data.get(key); if (local) local.merge(remote); else this.#data.set(key, new LWWRegister(this.id, remote)); } }
}

Next Steps

We now have two CRDTs in our toolbox: LWW Register and LWW Map. How do we actually use them? We’ll cover that in the next post: Building a Collaborative Pixel Art Editor with CRDTs Building a Collaborative Pixel Art Editor with CRDTs | jakelazaroff.com CRDTs sound cool, but how are they actually used? Let's learn by building a collaborative pixel art editor. jakelazaroff.com/words/building-a-collaborative-pixel-art-editor-with-crdts/ .


Read the original article

Comments

  • By aguacaterojo 2026-03-0322:274 reply

    I've spent a lot of time studying CRDTs & order theory in the last year & will publish an article too. Local-first apps are not easy to build, complex data structures (say, calendar repetitions with exceptions) become harder to model. Everything must converge automatically while clients can branch off.

    In general, you don't really get to compact tombstones meaningfully without consensus so you really are pushing at least remnants of the entire log around to each client indefinitely. You also can't easily upgrade your db you're stuck looking after legacy data structures indefinitely - or you have to impose arbitrary cut off points.

    List CRDTs - which text CRDTs are built from are probably unavoidable except for really really simple applications. Over the last 15 years they have evolved, roughly: WOOT (paper) -> RGA (paper) -> YATA (paper) / YJS (js + later rust port) -> Peritext (paper) / Automerge (rust/js/swift) -> Loro (Rust/js). Cola (rust) is another recent one. The big libs (yjs, automerge, loro) offer full doc models.

    Mostly the later ones improve on space, time & intent capture (not interleaving concurrent requests).

    The same few guys (Martin Kleppman, Kevin Jahns, Joseph Gentle, probably others) pop up all over the more recent optimisations.

    • By josephg 2026-03-0323:352 reply

      > In general, you don't really get to compact tombstones meaningfully without consensus so you really are pushing at least remnants of the entire log around to each client indefinitely.

      This is often much less of a problem in practice than people think. Git repositories also grow without bound, but nobody seems to really notice or care. For diamond types (my library), I lz4 compress the text content itself within the .dt files. The metadata is so small that in many cases the space savings from lz4 compression makes the resulting .dt file - including full change history - smaller than the document on disk.

      If anyone really cares about this problem, there are a couple other approaches:

      1. In our Eg-walker algorithm, we don't use stable guids to identify insert positions. Thats where other algorithms go wrong, because they need to store these guids indefinitely in case someone references them. For eg-walker, we just store relative positions. This makes the algorithm work more like git, where you can do shallow clones. And everything works, until you want to merge a branch which was split off earlier than your clone point. Then you should be able to download the earlier edits back to the common branch point and merge. To support merging, you also only need to store the metadata of earlier edits. You don't need the inserted content. The metadata is usually tiny (~1-4 bits per keystroke for text is common with compression.)

      2. Mike Toomim's Antimatter algorithm is a distributed consensus protocol which can detect when its safe to purge the metadata for old changes. It works even in the presence of partitions in the network. (Its conservative by default - if a partition happens, the network will keep metadata around until that peer comes back, or until some timeout.)

      > The same few guys (Martin Kleppman, Kevin Jahns, Joseph Gentle, probably others) pop up all over the more recent optimisations.

      Alex Good is another. He's behind a lot of the huge improvements to automerge over the last few years. He's wicked smart.

      • By aguacaterojo 2026-03-040:271 reply

        The man himself. Yeah agreed you guys have solved it. It is more a misfired crud dev instinct for me that sees it as wasteful. Just a different paradigm and not big in practice.

        I've got eg-walker & Diamond Types in my reading/youtube backlog. Diamond Types went further down the backlog because of "wip" on the repo! I will look into Antimatter too.

        • By josephg 2026-03-043:19

          > It is more a misfired crud dev instinct for me that sees it as wasteful.

          You're not alone. Lots of people bring this up. And it can be a real problem for some applications, if they store ephemeral data (like mouse cursor positions) within the CRDT itself.

      • By rudi-c 2026-03-042:241 reply

        re: git repositories

        That's partly because repositories rarely need to be cloned in their entirety. As such, even when you need to do it and it's a couple hundreds mb taking a few minutes, it's tolerated.

        In situations where a document needs to be cold loaded often, the size of the document is felt more acutely. Figma has a notion of GC-ing tombstones. But the tombstones in questions aren't even something that get created in regular editing, it happens in a much more narrow case having to do with local copies of shared components. Even that caused problems for a small portion of files -- but if a file got that large, it was also likely to be important.

        • By josephg 2026-03-043:272 reply

          > even when you need to do it and it's a couple hundreds mb taking a few minutes, it's tolerated.

          Well written CRDTs should grow slower than git repositories.

          > In situations where a document needs to be cold loaded often, the size of the document is felt more acutely.

          With eg-walker (and similar algorithms) you can usually just load and store the native document snapshot at the current point-in-time, and work with that. And by that I mean, just the actual json or text or whatever that the application is interacting with.

          Many current apps will first download an entire automerge document (with full history), then using the automerge API to fetch the data. Instead, using eg-walker and similar approaches, you can just download 2 things:

          - Current state (eg a single string for a text file, or raw JSON for other kinds of data) alongside the current version

          - History. This can be as detailed & go as far back as you want. If you download the full history (with data), you can reconstruct the document at any point in time. If you only download the metadata, you can't go back in time. But you can merge changes. You need history to merge changes. But you only need access to history metadata, and only as far back as the fork point with what you're merging.

          If you're working online (eg figma), you can just download history lazily.

          For client/server editing, in most cases you don't need any history at all. You can just fetch the current document snapshot (eg a text or json object). And users can start editing immediately. It only gets fancy when you try to merge concurrent changes. But thats quite rare in practice.

          • By rudi-c 2026-03-043:401 reply

            > If you're working online (eg figma), you can just download history lazily.

            You can download the history lazily, that's a special case of incrementally loading the document.

            If the history is only history, then sure. But I was understanding that we were talking about something that may need to be referenced but can eventually be GCed (e.g. tombstones-style). Then lazy loading makes user operations that need to reference that data go from synchronous operations to asynchronous operations. This is a massive jump in complexity. Incrementally loading data is not the hard part. The hard part is how product features need to be built on the assumption that the data is incrementally loaded.

            Something that not all collaborative apps will care about, but Figma certainly did, is the concept that the client may go offline with unsynced changes for an indefinite amount of time. So the tombstones may need to be referenced by new-looking old changes, which increases the likelihood of hitting "almost-never" edge cases by quite a bit.

            • By josephg 2026-03-0411:42

              > But I was understanding that we were talking about something that may need to be referenced but can eventually be GCed (e.g. tombstones-style)

              Yeah my point is that this is only a limitation of certain types of CRDTs. Automerge and Yjs both suffer from this.

              However, it’s not a requirement for anything built on egwalker. Or generally any CRDTs in the pure operation log style. Egwalker (and by extension, loro and diamond types) can generate and broadcast edits synchronously. Even without any history loaded.

          • By fellowniusmonk 2026-03-045:311 reply

            Thank you for your work. Diamond Types is probably my favorite piece of programming, transliterating the algo for line edits for a situation where I needed a a different algo for the line management itself, is probably the most rewarding deep dive on a algorithm I've ever done.

            • By josephg 2026-03-0411:402 reply

              I’m glad you enjoyed it! What did you build?

              • By fellowniusmonk 2026-03-0421:20

                Effectively a distributed ~OS that hands off between browser and cloud using websocket presence.

                So its like the human facing side of an OS where time, space (structure) and minds are all the only first class types and any apps exist as apptributes on the data itself.

                It's basically a document editor, cal, feed, chat app, etc. in one interface where temporal search and retrieve is built in.

    • By lifty 2026-03-048:441 reply

      Regarding the growing log in CRDTs, it doesn't have to be that way. Most of these popular CRDT libs use Merkle DAG only to build up the log of the changes. But there is a better way, you can combine a Merkle DAG for ordering + prolly trees for storing the actual state of the data. That gives you total ordering, an easy way to prune old data when you choose to, and an easy way to sync. Look into fireproof for how this is combined.

      Regarding distributed schemas, I agree, there's a lot of complexity there but it's worth looking into projects like https://www.inkandswitch.com/cambria/ and https://github.com/grafana/thema, which try to solve the problem. It's a young space though. If anyone else knows about similar projects or efforts, please let me know. Very interested in the space.

      • By josephg 2026-03-0411:461 reply

        Interesting! Do you mind explaining the idea in more detail?

        • By lifty 2026-03-0412:321 reply

          As far as I understand, libraries like Automerge use the Merkle DAG to encode a document as an immutable bundle of state changes aka operation log + the causal ordering which enables conflict free merging between multiple peers. The final document is reconstructed by combining the state transitions. So the Merkle DAG is both the state and the causal relationship between mutations which allows the merge "magic".

          Prolly trees allow you to store history independent data which is trivial to sync, diff and merge, regardless of insert order, merge order or originating peer. A Merkle DAG layered on top of prolly trees (event reference prolly tree roots) gives you causality so that peers can agree on a common view of history. So it's very useful because you can check integrity and travel in time, but you can keep as much of it as you want, because it's not necessary for constructing the current state. Prolly trees give you the current state and the easy syncing, diff,merge. So you can truncate the history as needed for your use case.

          For a production ready implementation of prolly trees you can check Dolt. For a combination of Merkle DAG (https://github.com/storacha/pail) and prolly trees you can check https://github.com/fireproof-storage/fireproof

          • By josephg 2026-03-052:551 reply

            Those are lovely data structures and I know about them. But how are you planning on using those data structures? What CRDT are you building?

            You might also be interested in Alex Good's Beelay algorithm: https://www.youtube.com/watch?v=neRuBAPAsE0

            • By lifty 2026-03-057:18

              Prolly trees can act as CRDTs if you have a merge function that always merges and doesn’t block.

              So my initial comment merely tried to make the point that there is a design space where you’re not stuck with the tradeoff of carrying the full Merkle DAG history just to be able to reconstruct the latest version of your document.

              Thanks for the video, will check it out!

    • By GermanJablo 2026-03-0420:13

      The tombstone problem is exactly why I built DocNode. You're right that you can't compact them without consensus, so DocNode just doesn't create them. It assumes a server decides the order, which is already the case in 99% of collaborative apps. The result: a doc stays the same size whether it was edited once or a thousand times. Yjs for the same doc grows every time someone types, pastes, or undoes. Check it out: https://docukit.dev

    • By fellowniusmonk 2026-03-0323:13

      I love diamond types personally, which I think Loro uses in certain cases. I find not only is DT fast under normal usage, allows for easy archiving and retrieval of past data but it also quickly outputs a per user casuality rendering that LLMs can pretty easily rectify in most cases. Compact and archival to flat files works very well.

  • By linkdd 2026-03-0321:35

    Last-Write-Win CRDTs are nice, but I wish the article talked about where CRDT really shine, which is when the state truly converge in a non-destructive way, for example:

    1) Counters

    While not really useful, they demonstrate this well:

      - mutations are +n and -n
      - their order do not matter
      - converging the state is a matter of applying the operations of remote peers locally
    
    2) Append-only data structures

    Useful for accounting, or replication of time-series/logs with no master/slave relationship between nodes (where writes would be accepted only on a "master" node).

      - the only mutation is "append"
      - converging the state is applying the peers operations then sorting by timestamp
    
    EDIT: add more

    3) Multi Value registers (and maps)

    Similar to Last-Write-Win registers (and maps), but all writes are kept, the value becomes a set of concurrent values.

    4) Many more...

    Each is useful for specific use cases. And since not everybody is making collaborative tools, but many are working on distributed systems, I think it's worth it to mention this.

    On another note, the article talks about state based CRDTs, where you need to share the whole state. In the examples I gave above, they are operation based CRDTs, where you need to share the operations done on the state and recompute it when needed.

    For example, in the Elixir ecosystem, we have Horde ( https://hexdocs.pm/horde/readme.html ) which allows distributing a worker pool over multiple nodes, it's backed by DeltaCrdt ( https://hexdocs.pm/delta_crdt/DeltaCrdt.html ).

    Delta-CRDTs are an optimization over state based CRDTs where you share state diffs instead of the whole state (described in this paper: https://arxiv.org/pdf/1603.01529 ).

  • By anematode 2026-03-0320:523 reply

    Enjoyed the article! As someone who's worked on bits of collaborative software (and is currently helping build one at work), I'd caution people from using CRDTs in general. A central server is the right tool for the job in most cases; there are certain things that are difficult to handle with CRDTs, like permissions and acquiring locks on objects.

    Edit: I had an excerpt here which I completely misread. Sorry.

    • By gritzko 2026-03-0320:562 reply

      Heh. Find how to grant permissions/ acquire lock in git. You can not. That is fundamental to distributed systems.

      Downside: harder to think about it all.

      Upside: a rocket may hit the datacenter.

      From what I remember about Figma, it can be proclaimed CRDT. Google Docs got their sync algorithm before CRDT was even known (yep, I remember those days!).

      • By josephg 2026-03-0323:40

        Early versions of google docs didn't even implement OT correctly. There were various cases where if two people bolded and unbolded text in a certain order, while connecting and disconnecting their wifi, the documents would go out of sync. You'd end up looking at different documents, even after everything reconnected.

        I (obviously) care a lot about fixing these sort of bugs. But its important to remember that in many applications, it matters a lot less than people think.

      • By anematode 2026-03-0321:061 reply

        Of course. But typical, live collaborative software doesn't need to be (and shouldn't be) decentralized. In such software it's annoying to not be able to (even speculatively) acquire unique access to an object. I'd be very surprised if Google Docs used CRDTs now.

        • By josephg 2026-03-0323:51

          Part time CRDT researcher here. I think CRDTs would work great for google docs. Google docs has a problem where if too many people open the document at the same time, it needs to lock the document. I assume this is because the document is "owned" by a single computer. Or they use DB-transactional ordering on edits.

          If I implemented google docs today, I'd use a CRDT between all the servers. This would let you have multi-master server scaling, and everything on the server side would be lock-free.

          Between the server and the client, CRDTs would be fine. With eg-walker, you can just send the current document state to the user with no history (until its actually needed). In eg-walker, you can merge any remote change so long as you have history going back to a common fork point. If merges happen and the client is missing history, just have them fetch the server for historical data.

          Alternately, you can bridge from a CRDT to OT for the client/server comms. The client side code is a bit simpler that way, but clients may need server affinity - which would complicate your server setup.

    • By epistasis 2026-03-0321:02

      Here's a 2019 Figma article on this, not sure if its representative of their current system

      > OTs power most collaborative text-based apps such as Google Docs. They’re the most well-known technique but in researching them, we quickly realized they were overkill for what we wanted to achieve ...

      > Figma's tech is instead inspired by something called CRDTs, which stands for conflict-free replicated data types. ... Figma isn't using true CRDTs though. CRDTs are designed for decentralized systems where there is no single central authority to decide what the final state should be

      https://www.figma.com/blog/how-figmas-multiplayer-technology...

    • By davexunit 2026-03-0413:38

      Permissions can be handled with capability systems. Keyhive [0] is the furthest along on this. I've also made my own prototype [1] showing how certificate capabilities can be composed with CRDTs.

      [0] https://www.inkandswitch.com/keyhive/notebook/

      [1] https://spritely.institute/news/composing-capability-securit...

HackerNews