<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://brainframe.tech/feed.xml" rel="self" type="application/atom+xml" /><link href="https://brainframe.tech/" rel="alternate" type="text/html" /><updated>2026-08-12T23:25:36+00:00</updated><id>https://brainframe.tech/feed.xml</id><title type="html">BrainFrame</title><subtitle>BrainFrame is a local-first personal knowledge management system and universal reader — read PDF, EPUB, DOCX, Markdown, archived web pages and more, take notes, and keep every note linked to the source it came from.</subtitle><entry><title type="html">Two Shoppers, One Grocery List, and a Missing Bag of Dog Food</title><link href="https://brainframe.tech/blog/2026/08/02/when-crdts-meet-structured-data/" rel="alternate" type="text/html" title="Two Shoppers, One Grocery List, and a Missing Bag of Dog Food" /><published>2026-08-02T14:00:00+00:00</published><updated>2026-08-02T14:00:00+00:00</updated><id>https://brainframe.tech/blog/2026/08/02/when-crdts-meet-structured-data</id><content type="html" xml:base="https://brainframe.tech/blog/2026/08/02/when-crdts-meet-structured-data/"><![CDATA[<p><a href="https://brainframe.tech/blog/2026/07/26/when-edits-collide/">My previous post</a>
demonstrated how to use <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-free Replicated Data
Types</a> (CRDTs)
to deal with concurrent editing of text between two offline devices, and making
it work. If you read that, you’ll get lots of warm fuzzy feelings about them,
and feel like they’re almost the answer to everything. If you haven’t read that
one, I do recommend it. This post will still be here.</p>

<p>Going against that grain, this article will show the walls that I’ve run into
with CRDTs as I work on <a href="https://brainframe.tech">BrainFrame</a>. The prior one had
a happy ending, while this one does not. Still, it’s worth the exploration to
understand <em>why</em> they aren’t the ultimate answer to life, the universe, and
everything.</p>

<p>I want you to come away from this article with enough knowledge to understand if
you should use CRDTs for your own project. And for those who are just curious,
I’d like to welcome you as well. While this post is unavoidably technical, I’ve
worked to keep it legible to those who do not live and breathe this work.</p>

<ul id="markdown-toc">
  <li><a href="#a-quick-tour-of-this-article" id="markdown-toc-a-quick-tour-of-this-article">A Quick Tour of This Article</a></li>
  <li><a href="#some-old-friends-go-shopping" id="markdown-toc-some-old-friends-go-shopping">Some Old Friends Go Shopping</a>    <ul>
      <li><a href="#who-writes-their-grocery-list-like-that" id="markdown-toc-who-writes-their-grocery-list-like-that">Who Writes Their Grocery List Like That?</a></li>
      <li><a href="#what-is-that-grocery-list-anyway" id="markdown-toc-what-is-that-grocery-list-anyway">What <em>is</em> that grocery list, anyway?</a></li>
    </ul>
  </li>
  <li><a href="#structured-data-and-crdts" id="markdown-toc-structured-data-and-crdts">Structured Data and CRDTs</a>    <ul>
      <li><a href="#where-are-we-going-again---how-last-writer-wins-works" id="markdown-toc-where-are-we-going-again---how-last-writer-wins-works">Where are we going again? - How “Last Writer Wins” Works</a></li>
      <li><a href="#what-are-we-buying---how-sets-and-lists-work" id="markdown-toc-what-are-we-buying---how-sets-and-lists-work">What are we buying? - How “Sets” and “Lists” Work</a></li>
      <li><a href="#where-is-everything-in-the-store---how-maps-work" id="markdown-toc-where-is-everything-in-the-store---how-maps-work">Where is everything in the store? - How “Maps” Work</a></li>
      <li><a href="#dogfood-breakdown" id="markdown-toc-dogfood-breakdown">Bob never bought dog food - When the tool broke down</a></li>
      <li><a href="#is-this-fixable" id="markdown-toc-is-this-fixable">Is this fixable?</a></li>
    </ul>
  </li>
  <li><a href="#im-not-making-an-overly-complicated-grocery-list-why-do-i-care" id="markdown-toc-im-not-making-an-overly-complicated-grocery-list-why-do-i-care">I’m not making an overly complicated grocery list. Why do I care?</a>    <ul>
      <li><a href="#yaml-frontmatter-should-obviously-be-structured-right" id="markdown-toc-yaml-frontmatter-should-obviously-be-structured-right">YAML Frontmatter should obviously be structured, right?</a></li>
      <li><a href="#ive-got-99-problems-and-theyre-all-schemas" id="markdown-toc-ive-got-99-problems-and-theyre-all-schemas">I’ve got 99 problems, and they’re all schemas</a></li>
      <li><a href="#if-i-defined-at-least-part-of-a-schema-wouldnt-that-fix-the-problem" id="markdown-toc-if-i-defined-at-least-part-of-a-schema-wouldnt-that-fix-the-problem">If I defined at least part of a schema, wouldn’t that fix the problem?</a></li>
      <li><a href="#the-final-breakdown" id="markdown-toc-the-final-breakdown">The final breakdown</a></li>
    </ul>
  </li>
  <li><a href="#how-this-broke-my-heart---final-thoughts" id="markdown-toc-how-this-broke-my-heart---final-thoughts">How this broke my heart - Final thoughts</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a>    <ul>
      <li><a href="#further-reading-popular-crdt-libraries" id="markdown-toc-further-reading-popular-crdt-libraries">Further Reading: Popular CRDT Libraries</a></li>
    </ul>
  </li>
</ul>

<h1 id="a-quick-tour-of-this-article">A Quick Tour of This Article</h1>

<p>Before we dive in: We cover a lot of ground in this article.</p>

<p>The next section, “Some Old Friends Go Shopping”, sets up the example that I’m
using through much of this post. It shows a simple grocery list, and gives us
the mental model to explore some concepts that will be used by both you and me
while we evaluate how best we can use CRDTs.</p>

<p>The following section (“Structured Data and CRDTs”) is all about CRDT data
types, and how they work with our example (and, with the library I’m using, how
they <em>don’t</em> work). If you’re already familiar with the common CRDT data types,
you can skip most of this section. I would, however, recommend picking back up
at “<a href="#dogfood-breakdown">Bob never bought dog food - When the tool broke down</a>”.
That’s the description of the first failure I had that made me stop and look to
see if I was doing the right thing.</p>

<p>The section after that (“I’m not making an overly complicated grocery list. Why
do I care?”) is a deeper dive that came out of that insight, and digs into such
things as schema vs schemaless YAML frontmatter, and when to choose one or the
other. I definitely don’t recommend skipping that one. It’s all about the
process I used when trying to make CRDTs and YAML frontmatter work for me in
BrainFrame. I wanted you to have this information to help you avoid making the
same mistakes I made.</p>

<p>The final section gives my lessons learned, and concrete advice about
whether or not you should even use CRDTs with structured data.</p>

<h1 id="some-old-friends-go-shopping">Some Old Friends Go Shopping</h1>

<p>Let’s reintroduce Alice and Bob. After they <a href="https://brainframe.tech/blog/2026/07/26/when-edits-collide/">edited the owl out of that
document</a>, they
discovered they enjoyed each other’s company, have moved in together, and are
planning their first ever grocery shopping trip. They need a grocery list. They
were even kind enough to give me a copy of it.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">store</span><span class="pi">:</span> <span class="s">Trader Mike's</span>
<span class="na">dairy</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">1 dozen eggs</span>
    <span class="pi">-</span> <span class="s">8oz cheese</span>
<span class="na">produce</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">5 apples</span>
    <span class="pi">-</span> <span class="s">5lb potatoes</span>
<span class="na">cereal</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">frosted wheat cereal</span>
    <span class="pi">-</span> <span class="s">honey nut shaped o's</span>
</code></pre></div></div>

<p>You might notice that (in the title) I promised some missing dog food, but it’s
not on the list. Hold that thought. We’ll <a href="#dogfood-breakdown">get to it</a> as
soon as we can.</p>

<h2 id="who-writes-their-grocery-list-like-that">Who Writes Their Grocery List Like That?</h2>

<blockquote>
  <p>Attention developers! This subsection is almost entirely to help our
non-developer friends understand that example better. If you already know
CRDTs and structured data, you should skip it, as it will be redundant for
you.</p>
</blockquote>

<p>Nobody I know will write out their grocery list looking like that, but I’m doing
it this time to provide an example we can work with throughout this article.</p>

<p>Calling back to the previous post, CRDTs are a long-winded way of saying “The
devices can always produce the same document, even if two people edit that
document at the same time in ways that produce a conflict.” That was all about
editing text, just like you might type up in Notepad, or TextEdit, or even vi.</p>

<p>The grocery list above is formatted the way it is to provide an example of
something called “structured data”. You’ve already worked with structured data
elsewhere: If you’ve ever edited a spreadsheet; if you’ve ever made your own
table in your favorite text editor; if you’ve ever made a to-do list; if you’ve
ever had your own grocery list. These are all structured data<sup id="fnref:structured-data" role="doc-noteref"><a href="#fn:structured-data" class="footnote" rel="footnote">1</a></sup>.
Nothing to be afraid of here, you’ve already used it. You just never knew it
would be called that.</p>

<h2 id="what-is-that-grocery-list-anyway">What <em>is</em> that grocery list, anyway?</h2>

<p>The list itself is formatted using something called
“<a href="https://en.wikipedia.org/wiki/YAML">YAML</a>”<sup id="fnref:yaml" role="doc-noteref"><a href="#fn:yaml" class="footnote" rel="footnote">2</a></sup>. It’s plain text, just like
<a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a> is plain text. It has some
rules about the formatting, but first and foremost YAML is meant for <em>people</em>,
just like Markdown. It’s why YAML and Markdown make for a powerful combination
for notes. It might look ugly (okay, no might, it does), but it is very readable
by people. You can see which store the list is for, which aisles in the store
Alice and Bob are going to visit, and what they’re going to get from each aisle.</p>

<p>What makes this useful for us is that each of these things can be handled
differently than normal text. For instance, it’s possible to maintain a
collection across devices and have them automatically sync up in ways that just
plain text has a hard time with. These special cases allow us to do things that
are otherwise very difficult to manage. I’m going to go over a few of the
special things we can do next. And when I get to the end of it, I’m going to
finally solve the mystery of the missing dog food. Explaining it just requires a
bit of understanding first.</p>

<h1 id="structured-data-and-crdts">Structured Data and CRDTs</h1>

<p>CRDTs are well-studied. Because of the properties they have, it’s possible to
write code around structured data that does things that are surprisingly easy
with them, and difficult without them. We’re going to go over some of the
possibilities here, but the <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Wikipedia article about
them</a> has a
more comprehensive list.</p>

<p>I’m going to focus on the ones that affect both our grocery list above, and
BrainFrame overall. By the time you get through these, you could be forgiven for
thinking I’ve forgotten about the overall idea of lists in YAML. I haven’t.
We’re going to be treating the aisle listings as sets instead in just a moment.
The list idea is what we’d call a “sequence CRDT”, and one example of that is
just freeform text. After all, that text is just a list of characters kept in
order. To learn more about them, I’m going to refer you to my <a href="https://brainframe.tech/blog/2026/07/26/when-edits-collide/">previous
post</a>.</p>

<h2 id="where-are-we-going-again---how-last-writer-wins-works">Where are we going again? - How “Last Writer Wins” Works</h2>

<p>Check out that grocery list again, specifically the very first line:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">store</span><span class="pi">:</span> <span class="s">Trader Mike's</span>
</code></pre></div></div>

<p>The name of the store that Alice and Bob are going to is “Trader Mike’s”. What
happens when the edits come in? Alice decides to change the store to “Organics R
Us”, while Bob changes it to “Food Tiger”. If we use the same tools from the
sequence CRDTs, we’ll wind up with something like “Organics R UsFood Tiger”.
Alice and Bob can still have their discussion and figure out the correct store
name, but what if they didn’t have to?</p>

<p>We can simply say that the last edit is the one that will always be the one that
gets chosen. If Alice made her change at 12:01pm, and Bob made his change at
12:02pm, then we can assume that his change is the one that everything should
reflect when everything synchronizes. We can then set the store name to be “Food
Tiger” just because his edit was the most recent one.<sup id="fnref:hlc-vs-wall-clock" role="doc-noteref"><a href="#fn:hlc-vs-wall-clock" class="footnote" rel="footnote">3</a></sup></p>

<p>In CRDT terms, we call this “Last Writer Wins”, or “LWW”. Generally speaking,
for freeform text, we don’t want LWW. For a single value that should always
match some piece of reality (like the name of the store), using LWW is a valid
choice to make. While this example is a little forced for the store name, other
things matter more, especially for our notes that we want to make. For example,
we might want a <code class="language-plaintext highlighter-rouge">status</code> field to always be something the rest of the app can
recognize, like “work-in-progress” or “in-review” or “published”.</p>

<h2 id="what-are-we-buying---how-sets-and-lists-work">What are we buying? - How “Sets” and “Lists” Work</h2>

<p>Once more back to the grocery list, where we can see things like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="pi">-</span> <span class="s">1 dozen eggs</span>
    <span class="pi">-</span> <span class="s">8oz cheese</span>
</code></pre></div></div>

<p>Here, I need to be precise, and that precision can be difficult to see. In YAML
terms, this is an <em>ordered</em> list. If code were to redraw this, it should always
do so in the <em>same</em> order. For the grocery list, this can genuinely matter. It’s
possible (especially depending on how well Alice and Bob know the store) to make
the items in the aisle appear in the order that they can be found in the aisle.</p>

<p>Instead, for BrainFrame, we’re going to look at this as a set. This removes the
ordered aspect, and adds in another constraint that matters for BrainFrame, but
not for the grocery list: A set’s items are always unique. A list, for contrast,
can have 12 entries that each say “1 dozen eggs”, but the set can only have one
entry that says this. In YAML, they are going to be written down the same way:
No difference exists when you look at YAML. It’s all about what the code does
with it that makes it different.</p>

<p>Compare and contrast these two: On a grocery list, you will change the beginning
of the string to reflect a quantity (“2 dozen eggs” instead of “1 dozen eggs”).
For BrainFrame, the most likely use for this sort of thing will be things like
tags on the article, which should always be unique. For both of these, a set can
actually make sense. So can a list. And that is where the problem truly lies.</p>

<p>This is where <em>intent</em> matters, which is the thing that computers are bad at
guessing. I am choosing to treat these ordered lists as sets because I believe
that is the intent most of the time. That belief could be wrong, though, and I
can’t know until users start working with the program.</p>

<p>The inability to determine the intent from the YAML also means that both of
these distinct things are very easily placed under the same conceptual category.</p>

<h2 id="where-is-everything-in-the-store---how-maps-work">Where is everything in the store? - How “Maps” Work</h2>

<p>A little more YAML (and its explanation) before we can finally figure out why
Bob never bought dog food.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dairy</span><span class="pi">:</span>
<span class="na">produce</span><span class="pi">:</span>
<span class="na">cereal</span><span class="pi">:</span>
</code></pre></div></div>

<p>These are all the names of the aisles that Alice and Bob are planning to visit
when they go to the store. They also happen to represent the most complex
concept that we’ve hit so far: Maps (and I don’t mean the thing that has all the
roads on it).</p>

<p>Broadly speaking, maps use a string (usually a single word, such as the name of
a grocery store aisle) to say what can be found there (such as “8oz cheese” in
the “dairy” aisle). We call that string a “key”, and we call what it points to a
“value”. You can see it very easily yourself. Here’s the dairy aisle again, just
to refresh your memory:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dairy</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">1 dozen eggs</span>
    <span class="pi">-</span> <span class="s">8oz cheese</span>
</code></pre></div></div>

<p>The dairy aisle contains two items in its set. In this form, it is very easy to
see and understand. It becomes harder when you understand that the things
pointed to by the key aren’t limited to being sets. The entire grocery list is a
map. It has a “store” key, as well as keys for each aisle. Each aisle could be
further broken down. For instance, the following would also be valid for the
grocery list:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">dairy</span><span class="pi">:</span>
    <span class="na">eggs</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">1 dozen large</span>
        <span class="pi">-</span> <span class="s">1 dozen brown</span>
    <span class="na">cheese</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">8oz cheddar</span>
        <span class="pi">-</span> <span class="s">4oz mozzarella</span>
</code></pre></div></div>

<p>In this case, Alice and Bob have created a map of maps to make their shopping
trip hyper efficient. And they can continue making ever deeper maps. Each value
that they point to can be a string (like the store name), a map, or a set of
items to buy in that area of the store.</p>

<p>Critical behavior note: In any given map, keys are unique. To use our dairy
example, you can only have <em>one</em> <code class="language-plaintext highlighter-rouge">cheese</code> key. When programming, the result of
trying to add a second <code class="language-plaintext highlighter-rouge">cheese</code> key will destroy the original value and replace
it with the new value.</p>

<p>Maps are one of the most useful tools that we software developers have. It’s
very easy for us to reach for them for many different uses. Sometimes, they feel
right at first, and then we find that they’re the wrong tool, and we have to
change what we are doing. If we’re very lucky, we find it out early enough that
we’ve only wasted an hour. Sometimes, the work can take weeks to unravel.</p>

<p>In my case, the pain wasn’t just the lost time I spent chasing structured data
and CRDTs. It was the lost vision. To show why it got lost, it’s time to explain
the missing dog food.</p>

<h2 id="dogfood-breakdown">Bob never bought dog food - When the tool broke down</h2>

<p>Alice and Bob have made it to the grocery store, and they want to make it a
quick trip. They split up, and their devices go offline. While they’re each
collecting the items from their part of the list, they realize they forgot to
add something: pet food! Alice adds the following YAML on her phone:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pet_food</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">canned cat food</span>
</code></pre></div></div>

<p>Bob adds the following YAML on his phone:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pet_food</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">dry dog food</span>
</code></pre></div></div>

<p>They’ve each got it in their notes on their devices. Before they actually pick
things up from the shelves, their devices manage to synchronize with each other.
If everything is behaving correctly, the results <em>should</em> look like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pet_food</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">canned cat food</span>
    <span class="pi">-</span> <span class="s">dry dog food</span>
</code></pre></div></div>

<p>A new map key was added to both devices. Each device added a set with a single
item to that map key. The <em>order</em> of the set on your screen can be reversed from
what I show above, but both devices should have them visible in the same order.
CRDTs manage this behavior well for structured data.</p>

<p>Plans rarely survive first contact with reality. When I tested this exact
scenario, I got a very different result. Here’s what I saw:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pet_food</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">canned cat food</span>
</code></pre></div></div>

<p>The dog food never made it into the resulting map after the synchronization
occurred. I could review the changes that each side was making. I could see the
dog food on the one side, and the cat food on the other, and the dog food was
simply <em>gone</em> from the result, as if it had never existed.</p>

<p>If Alice and Bob stick to their list, then by the time they get to the pet food
aisle, Bob will never buy the dog food. The exact order of operations
matters<sup id="fnref:order-of-operations" role="doc-noteref"><a href="#fn:order-of-operations" class="footnote" rel="footnote">4</a></sup>, so let’s spell it out here:</p>

<ol>
  <li>The code sorted the changes, and Bob’s edit was determined to be first.</li>
  <li>The code made a new key in the map from Bob’s edit, named “pet_food”.</li>
  <li>The code then assigned Bob’s set as the value for that key - The “dry dog
food”</li>
  <li>The code saw Alice’s new key “pet_food”</li>
  <li>The code then set that key with Alice’s set - The “canned cat food”.</li>
</ol>

<p>Remember: Keys <em>must</em> be unique in a given map. So when the code acted the way
it did, it overwrote the previous “pet_food” key wholesale. Instead of merging
the two sets together (the preferred and arguably correct behavior), Bob’s dog
food was relegated to the dust bin.</p>

<p>I lost data. And data loss is the one thing I can’t accept.</p>

<h2 id="is-this-fixable">Is this fixable?</h2>

<p>And this is the most important question: Can it be fixed? The answer is an
unsatisfying “maybe”. This is where the way that the CRDT library is written
matters; I’m not sure I could do better than the author on this, which is why
the answer is a “maybe”. On the plus side, even if it turns out it can’t be
fixed, it can be detected, which is arguably more important for figuring out if
your CRDT library is okay.</p>

<p>This is the most technically challenging part of this entire article, but it’s
also the most critical for understanding what’s gone wrong.</p>

<blockquote>
  <p>I hate to tell my non-developer readers to pass it over, but this section is
genuinely a lot of geek-speak, and I can’t reduce that. Feel free to move on
to the next section. If you <em>do</em> skip, take this one note with you: CRDTs, as
a tool, are great at what they do. The specific tool I’m using didn’t live up
to those expectations.</p>
</blockquote>

<p>When working with CRDTs, everything gets a unique identifier. With the library
I’m using, those identifiers are called “<a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">Universally Unique
Identifiers</a>”
(UUIDs). You’ve probably seen them at some point; they look like this:
<code class="language-plaintext highlighter-rouge">60c91291-aaad-4f1e-98b0-98f21a603497</code>. Each map key in the grocery list has
one. So <code class="language-plaintext highlighter-rouge">store</code>, <code class="language-plaintext highlighter-rouge">dairy</code>, and <code class="language-plaintext highlighter-rouge">pet_food</code> all have one.</p>

<p>The critical difference is that the others were agreed upon by the devices
<em>before</em> everything went offline. This is best described by stating the order of
operations explicitly. For this list, it’s entirely arbitrary who did what. What
matters is how their devices dealt with the edits.</p>

<ol>
  <li>Alice created the file.</li>
  <li>Both devices synchronized. Bob now has a copy of Alice’s file.</li>
  <li>Bob added the <code class="language-plaintext highlighter-rouge">store</code> key.</li>
  <li>Both devices synchronized. Alice now has a copy of Bob’s store key. This
means that the UUID for this key is the same on both of their devices.</li>
  <li>Repeat steps 3 and 4 for everything on the list: the keys, the values, the
sets, everything. <em>Except</em> for the pet food.</li>
  <li>Alice and Bob go to the store, and their devices go offline.</li>
  <li>Alice adds the <code class="language-plaintext highlighter-rouge">pet_food</code> key. This generates a new UUID for <em>her</em> <code class="language-plaintext highlighter-rouge">pet_food</code>
key.</li>
  <li>Bob adds the <code class="language-plaintext highlighter-rouge">pet_food</code> key. This generates a new UUID for <em>his</em> <code class="language-plaintext highlighter-rouge">pet_food</code>
key.</li>
  <li>The devices come back online and synchronize. At this time, before the map
gets re-evaluated, <em>both</em> copies of the <code class="language-plaintext highlighter-rouge">pet_food</code> key exist.</li>
  <li>The map gets re-evaluated, and the key (which must be unique, remember) gets
exactly one winner.</li>
  <li>Bob’s “dry dog food” entry is lost, because the differing UUIDs pointed to
<em>different</em> “pet_food” keys. CRDTs themselves did not prevent the merge;
this was the way the implementation was executed on this library.</li>
</ol>

<p>The poor behavior here is subtle, but important: The library treated the
“pet_food” keys as different when deciding to merge (because of the different
UUIDs), but then treated them as the same when enforcing uniqueness. That
contradiction is what dropped the dog food on the floor.</p>

<p>I am speculating here, but I would be surprised if this isn’t a common bug with
implementations doing what I’m doing. CRDTs absolutely require the
identity<sup id="fnref:identity" role="doc-noteref"><a href="#fn:identity" class="footnote" rel="footnote">5</a></sup>, and this is an easy bug to miss if you’re not looking for
it.</p>

<p>Before moving on, an obvious question you’re asking me right now: How do I check
my library for this? I’m going to answer with a bit of pseudocode. You need to
translate this for your preferred programming language and the library you are
using. Here’s how you can do it in about 40 lines of code:</p>

<ul>
  <li>Generate a device id for your CRDT library, call it “peerA”</li>
  <li>Generate a second device id for your CRDT library, call it “peerB”</li>
  <li>Generate a document for your CRDT library, call it “doc”</li>
  <li>Make sure that both peerA and peerB are working with your same document. This
is likely some sort of “import/export changes” method, or even an “exchange
document” method.</li>
  <li>Generate an Observed-Remove Map for your CRDT library and document, using
peerA, call it “map”</li>
  <li>Generate an Observed-Remove Set for that map on peerA, call it “tags”.</li>
  <li>Add the tag “tag1” to that set.</li>
  <li>Generate an Observed-Remove Map for your CRDT library and document, using
peerB, call it “map”</li>
  <li>Generate an Observed-Remove Set for that map on peerB, call it “tags”.</li>
  <li>Add the tag “tag2” to that set.</li>
  <li>Have both peers exchange their data (import/export changes, exchange document,
etc, same as the document exchange above)</li>
  <li>View the resulting document from both peers. If your library handles it well,
you will see both “tag1” and “tag2” in the result. If it does not, you will
see only one of the tags appear in the document on both peers.</li>
</ul>

<p>Once done, you’ll know if you are subject to the same limitations I’m
describing. Two notes about this:</p>

<ul>
  <li>Note that the names are <em>deliberately</em> identical. You need to generate
conflicting names that can overwrite each other to prove what happens with
your library of choice.</li>
  <li>The pseudocode is using an Observed-Remove Map<sup id="fnref:or-map" role="doc-noteref"><a href="#fn:or-map" class="footnote" rel="footnote">6</a></sup>. That’s a deliberate
choice. Don’t just make sure the test uses it: Make sure your actual code that
you will ship uses it as well. If your library uses a default map that doesn’t
use Observed-Remove, you may believe that everything is okay even though
you’re not <em>actually</em> using the Observed-Remove map.</li>
</ul>

<p>In my case, I am looking for it because of BrainFrame itself. I need the things
I’ve been describing, but the ways in which I need them can be surprising.</p>

<h1 id="im-not-making-an-overly-complicated-grocery-list-why-do-i-care">I’m not making an overly complicated grocery list. Why do I care?</h1>

<p>Here’s the surprising thing: Even if you’re not making a grocery list, you <em>are</em>
using all those same concepts when you make your notes. I’ll prove it with this
sample note:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">Review of The Tell-Tale Heart</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">Edgar Allan Poe</span>
<span class="na">first_published</span><span class="pi">:</span> <span class="s">January, </span><span class="m">1843</span>
<span class="na">migrated_to_notion</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">status</span><span class="pi">:</span> <span class="s">work-in-progress</span>
<span class="na">tags</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">horror</span>
    <span class="pi">-</span> <span class="s">fiction</span>
<span class="nn">---</span>
This was a lighthearted romp through
the madness that descended on the
protagonist after killing someone.
I really enjoyed it.
</code></pre></div></div>

<p>Instead of calling it a grocery list, we call it “YAML frontmatter”. It’s
structured text that allows you to make tools that work on your notes. For
example, you may want to key off of the status. When that gets moved to
“published”, you actually post the review somewhere.</p>

<p>You can see all the elements we discussed earlier: the whole map, the set of
tags, the status being a “last writer wins” value.</p>

<h2 id="yaml-frontmatter-should-obviously-be-structured-right">YAML Frontmatter should obviously be structured, right?</h2>

<blockquote>
  <p>This subsection is a little more dry than most, and can be summed up very
simply: Every field that appears in the frontmatter needs a type, and I’m now
explaining how I chose the types. If you’re comfortable already with choosing
types, or simply not interested in how to do so, feel free to skip this
section.</p>
</blockquote>

<p>Thanks to everything you’ve learned throughout this article, you can see how
that YAML frontmatter is structured data, and BrainFrame should absolutely honor
that structure. I agree with you, and here’s what I would do for that document:</p>

<ul>
  <li>The entirety of the frontmatter is a map. You can see the keys and values
already, so this makes sense.</li>
  <li>title is a string. I would treat this as text, instead of “last writer wins”.
Multiple people and devices might change that title before it is finally
settled.</li>
  <li>author is unlikely to ever change for this example. It could be a “last writer
wins”, or it could be a string. I’m choosing a string (which is handled as
text by the CRDT library), but either answer could easily be valid. The real
danger is coming up.</li>
  <li>first_published is a “last writer wins”. If somebody ever finds an earlier
publication date, then we need to adjust it in one change. It looks like a
string, but it’s never going to be partially replaced or updated.</li>
  <li>migrated_to_notion is a “last writer wins”. It’s either been migrated, or it
hasn’t.</li>
  <li>status is a “last writer wins” as well. It’s always one complete status, never
a mish-mash of two or more.</li>
  <li>tags is a set of strings.</li>
</ul>

<p>Looking at this, YAML frontmatter practically <em>begs</em> to be treated as structured
data. But looking back to the grocery store reveals an issue that makes it
problematic.</p>

<p>It’s always easy, right up until it isn’t.</p>

<h2 id="ive-got-99-problems-and-theyre-all-schemas">I’ve got 99 problems, and they’re all schemas</h2>

<p>Remember: YAML is for people, first and foremost. Us people are messy, and we
come up with new ideas all the time. It is trivial for us to add something to
that YAML frontmatter that we had never considered. For instance, maybe we
choose to add a link to the <a href="https://www.gutenberg.org/files/2148/2148-h/2148-h.htm#chap2.20">Project Gutenberg page for The Tell-Tale
Heart</a> so we
can download it. That could look like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s">https://www.gutenberg.org/files/2148/2148-h/2148-h.htm#chap2.20</span>
</code></pre></div></div>

<p>While I’m writing the code, I have no idea what keys might be placed in the YAML
frontmatter. The URL is just one example. Maybe the user is doing a survey of
the favorite colors of the authors, so decides to add a <code class="language-plaintext highlighter-rouge">favorite_color</code> key. In
order for me to set up the code to handle structured data, I have to tell the
code what that structured data actually is. In software development, we call
that a “schema”.</p>

<p>This lets us say things like “author is a string, handle it like text” and
“status is a ‘last writer wins’”. The code then honors those choices, and
everything works happily for the choices we have defined, as long as the user
uses those things in the same way.</p>

<p>What happens when the user decides to add that “favorite_color” key? Even worse,
what happens when they do it on their phone, and then switch to their currently
offline e-ink reader and add it there as well (since they didn’t see it yet on
their e-ink reader)? We now have two keys, with two different identities, on two
different devices. Something is going to get lost.</p>

<p>What about that “author” key? What if we have two documents that try to do
different things here: one has only one author, while another has multiple
authors? Because we’re dealing with code, we have to make a choice, and a bad
choice here mangles what the user actually wants. The author key can’t have two
shapes in the code: It has to be either a single string, or a set. And if the
user tries to change the shape without a developer updating the code, their data
is going to get (at best) garbled. At worst, it could be completely lost, and
without any explanation as to why the data got lost.</p>

<p>That’s the real danger: In order to have everything be structured data, we have
to commit to a schema that can’t ever cover everything that the user needs.</p>

<h2 id="if-i-defined-at-least-part-of-a-schema-wouldnt-that-fix-the-problem">If I defined at least part of a schema, wouldn’t that fix the problem?</h2>

<blockquote>
  <p>This subsection is almost an engineering journal discussing the options that
got explored and why they didn’t work. I don’t want to cheat my readers out of
this information, but I also acknowledge that it might be less interesting to
some of you. Feel free to skip forward to the next header if this sort of
discussion doesn’t give you any value.</p>
</blockquote>

<p>YAML gives us the ability to put structure on the text that goes into the
frontmatter. Using that knowledge, we could go for a hybrid approach: Define a
list of keys and how we will handle them, and leave the rest alone as user
input. If we do that, we can add in keys like title, aliases, url, author, tags,
status, and a few others.</p>

<p>Unfortunately, this still is broken from the user’s perspective. As the
developer, I will miss more keys than I ever capture, and the user will still be
able to create more. What’s more, that same data loss scenario comes back when
somebody adds the same key on two separate devices and then synchronizes them.
Telling the user “You used the tool wrong, so you lost data, sorry” is never a
sentence I want to say.</p>

<p>And with that, the complexity is just beginning. I can say “These are all the
keys that are supported by the YAML frontmatter, and no other key will ever be
allowed unless/until the code is updated to support it.” By doing that, I’ve
just limited an incredibly useful tool into being something that <em>I</em> control and
decide, not the user.</p>

<p>I could make two separate front matter sections. The first one will be the one
that is all of the structured data that BrainFrame supports, and the second will
be the one that the user makes choices about. The problem then becomes “Which
frontmatter section is being looked at?” What if the user doesn’t add the
BrainFrame supported section, but just adds their own section? They don’t get
the benefit of structured data from BrainFrame at all then. And BrainFrame,
seeing only one frontmatter section, will have every reason to act as if that
frontmatter belongs to the BrainFrame-supported structured data, not the user’s
frontmatter.</p>

<p>As a final option, I could make the schema into something that is stored in a
note. I could ask the user to take the time to update their schema every time
they want to add a new frontmatter field. It’s not impossible, but it’s pretty
horrible to tell a non-developer that they have to carefully update a file just
to protect their data from being destroyed.</p>

<h2 id="the-final-breakdown">The final breakdown</h2>

<p>In the end, one surprising revelation is what finally broke me: Comments. Let’s
take that sample from above, and add something for us to do later:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">Review of The Tell-Tale Heart</span>
<span class="na">author</span><span class="pi">:</span> <span class="s">Edgar Allan Poe</span>
<span class="c1"># need to double-check the first_published data</span>
<span class="c1"># and make sure it's accurate</span>
<span class="na">first_published</span><span class="pi">:</span> <span class="s">January, </span><span class="m">1843</span>
<span class="na">migrated_to_notion</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">status</span><span class="pi">:</span> <span class="s">work-in-progress</span>
<span class="na">tags</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">horror</span>
    <span class="pi">-</span> <span class="s">fiction</span>
<span class="nn">---</span>
This was a lighthearted romp through
the madness that descended on the
protagonist after killing someone.
I really enjoyed it.
</code></pre></div></div>

<p>Comments have no structure. They attach to nothing. The comment I added above
could be anywhere in the frontmatter. Anything that changes that frontmatter
could cause BrainFrame to need to regenerate the entire frontmatter section. The
comment could be moved around or even destroyed.</p>

<p>My promise to the user is simple: Your data is protected. I will not destroy it.
I will not lose it.</p>

<p>Between the schema issues (especially coordinating that schema across devices
which may or may not be accessible when the updates need to happen) and the
ability to accidentally destroy the data, I hit the wall I could not overcome.</p>

<h1 id="how-this-broke-my-heart---final-thoughts">How this broke my heart - Final thoughts</h1>

<p>My vision did not survive the constraints I’d placed on myself. I want to have
everything be perfect. I want the user to have the ability to make a schema that
suits <em>them</em>. I want to support that schema by having everything be treated
properly: Sets, Maps, Last Writer Wins, all of it. I want the user to get all of
the possible benefits out of YAML frontmatter they could, fully supported by
BrainFrame and CRDTs doing things that other solutions just don’t handle as
well. In other words, I want to have my cake and eat it too.</p>

<p>I thought I went into this with my eyes wide open. As it turned out, I was
committing one of the cardinal sins of software development. I had my vision. I
had a tool. And I got attached to using that tool to make my vision. And when
the tool couldn’t support my vision, it hurt in ways that surprised even me. I’m
hoping that by sharing these lessons with you, I’ll exorcise some of my own
demons and let myself move forward again.</p>

<p>Allow me to shorten these lessons for you now.</p>

<ul>
  <li>Don’t become attached to your tools. When they let you down (and sooner or
later, they will, because no tool is perfect), it will hurt you deeply and
keep you from moving forward.</li>
  <li>Learn the rough edges of your tools as quickly as possible. The sooner you
know them, the sooner you can prevent yourself sailing off the edge of the map
with no way back.</li>
  <li>Concurrent replicas are <em>hard</em>, and in surprising ways. No matter how much you
may prepare yourself, it is easy to find yourself in a tough spot, trying to
find a way out of it so your project can move forward. Don’t give up. The path
out may not be clear, but it <em>does</em> exist.</li>
  <li>CRDT Maps, Sets, Last Writer Wins, and other data types are the right choice
for structured data as long as you have a well-defined schema. If your schema
is not something you can fully define (such as what happens when the user can
make any field they choose), I would avoid using those types. The difficulty
of keeping everything in sync when there is no governance on what these things
actually are is a problem with no solution I can find. For your use case, you
can possibly still use CRDTs for freeform text, like I am. That’s a
surprisingly flexible option.</li>
</ul>

<p>In the end, I had to make a choice that I hate, but it’s my way out of the
weeds: YAML frontmatter, while it is structured data, is also just text. I’m
going to treat it as such, and provide tools to help my users make the best
sense out of it that they can should things ever get into a weird state. But I’m
not going to drop their data, and that’s the important part.</p>

<p>If you have solutions to what I’ve described, or just want to commiserate about
the difficulties, I’m on Mastodon as <a href="https://c.im/@pedersen">@pedersen@c.im</a>.</p>

<p>I’ll talk with you soon.</p>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://brainframe.tech/">BrainFrame</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-Free Replicated Data
Types</a></li>
  <li><a href="https://sookocheff.com/post/time/hybrid-logical-clocks/">Hybrid Logical
Clocks</a></li>
  <li><a href="https://www.inkandswitch.com/essay/local-first/">Local First</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a></li>
  <li><a href="https://www.gutenberg.org/files/2148/2148-h/2148-h.htm#chap2.20">Project Gutenberg: The Tell-Tale
Heart</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">Universally Unique
Identifiers</a></li>
  <li><a href="https://brainframe.tech/blog/2026/07/26/when-edits-collide/">What’s the Right Answer When Both Edits Are
Valid?</a></li>
  <li><a href="https://en.wikipedia.org/wiki/YAML">YAML</a></li>
</ul>

<h2 id="further-reading-popular-crdt-libraries">Further Reading: Popular CRDT Libraries</h2>

<p>This is <em>not</em> an exhaustive list, don’t treat it as such. These are just some
that might be useful to you:</p>

<ul>
  <li><a href="https://automerge.org/">Automerge</a></li>
  <li><a href="https://pub.dev/packages/crdt_lf">crdt_lf</a></li>
  <li><a href="https://yjs.dev/">Yjs</a></li>
</ul>

<h1 class="no_toc" id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:structured-data" role="doc-endnote">
      <p>I’m being deliberately a bit handwavy here. Some of these
more correctly fall into a category known as “strongly structured data”,
while others fall into the category known as “semi-structured data” or
“loosely structured data”. Arguably, something like a todo list could even
be labeled as “unstructured data”, and would be treated that way in rigorous
practice. However, with the way I’m using it here, it fits into the
“semi-structured” bucket. There are important technical differences between
the three, but this post isn’t about those differences. We’re going to
explore why any kind of structured data coupled with freeform text is a
problem for CRDTs, so I’m going to continue to ignore those differences for
this article. <a href="#fnref:structured-data" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:yaml" role="doc-endnote">
      <p>I hate leaving acronyms unexplained, but this one is basically a joke
for computer programmers. YAML stands for “YAML Ain’t Markup Language”. It
can do a lot, and the <a href="https://en.wikipedia.org/wiki/YAML">Wikipedia
article</a> goes into it in depth. For now,
just know that it’s something simple for us humans to read. <a href="#fnref:yaml" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:hlc-vs-wall-clock" role="doc-endnote">
      <p>When you’re familiar with CRDTs, you’ll catch a
simplification here that could throw you off. I’m using the wall clock as a
stand-in for the <a href="https://sookocheff.com/post/time/hybrid-logical-clocks/">Hybrid Logical
Clock</a>, or HLC. An
HLC (which is what BrainFrame is using behind the scenes) is how the
ordering actually gets determined, not the clock on the wall. I’m using the
clock on the wall as an easier way to understand the ordering. HLC is the
decider, but that comparison is harder to make in a post like this. If
you’re not already familiar with HLCs, it’s worth noting that they do <em>not</em>
guarantee a match to the actual time. Frequently, they will line up.
However, computerized time-keeping is imperfect, and drifts (or is manually
configured to be wrong). Even though HLCs record that time, it’s important
to note that they work strictly for ordering, and do not (ever) guarantee
that they are actual timestamps. Treating them that way will only make your
life more difficult. For still more information, see the <a href="https://sookocheff.com/post/time/hybrid-logical-clocks/">article about
them</a>. <a href="#fnref:hlc-vs-wall-clock" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:order-of-operations" role="doc-endnote">
      <p>This order of operations hides something subtle that you
might have carried forward from the “Last Writer Wins” topic: The way I’ve
phrased things suggests an order based on time. Alice edited, Bob edited,
and I’ve kept that ordering because it’s alphabetical, not because of any
perceived order. What matters in this case is that the edits were made while
both devices couldn’t talk with each other, and then the code had to decide
what to do with those edits when synchronization occurred. The code in
question did its own internal sorting to determine which operations happened
in which order. It could have just as easily gone the other way, where Alice
might have not picked up the cat food. Don’t take my arbitrary alphabetical
ordering as the canonical order that CRDTs in general would apply things.
Your individual CRDT library might use a different clock or a different
mechanism for sorting. <a href="#fnref:order-of-operations" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:identity" role="doc-endnote">
      <p>Don’t assume that “identity” must mean UUID. It could be a counter,
with each character or key getting a number. It could be a UUID. It could be
a different type of identifier entirely. This is an implementation detail of
the library. It would be good for you to know what your CRDT library is
doing under the hood, but CRDTs don’t mandate the use of UUIDs. They’re just
convenient for us. <a href="#fnref:identity" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:or-map" role="doc-endnote">
      <p>I didn’t cover Observed-Remove in the basic data types because they
added length to this post without enhancing anything from the basics
section. The idea behind them is that no peer should ever remove data unless
it saw that data before trying to remove it. In the bit of pseudocode that
brought you to this footnote, if you were to tell peerB to remove “tag1”
from the set <em>before</em> the final exchange, the tag should survive. If you
were to tell peerB to remove “tag1” from the set <em>after</em> the final exchange,
the tag should be deleted. For more detailed information, see
<a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#OR-Set_(Observed-Remove_Set)">Wikipedia</a>
and its references. <a href="#fnref:or-map" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="crdt" /><category term="sync" /><category term="offline" /><category term="localfirst" /><summary type="html"><![CDATA[Learn how to use structured data with conflict-free replicated data types. Decide if you should even use them at all.]]></summary></entry><entry><title type="html">What’s the Right Answer When Both Edits Are Valid?</title><link href="https://brainframe.tech/blog/2026/07/26/when-edits-collide/" rel="alternate" type="text/html" title="What’s the Right Answer When Both Edits Are Valid?" /><published>2026-07-26T16:00:00+00:00</published><updated>2026-07-26T16:00:00+00:00</updated><id>https://brainframe.tech/blog/2026/07/26/when-edits-collide</id><content type="html" xml:base="https://brainframe.tech/blog/2026/07/26/when-edits-collide/"><![CDATA[<p>“The colorful owl sat.”</p>

<p>One single, simple sentence. Perfectly innocuous. Unremarkable even. And yet,
somehow, this one little sentence is the introduction to an entire class of
problem that has a major impact on BrainFrame (the local-first, own-your-notes
app that I’m building). That impact is large enough that I’ve spent literal
weeks wrapping my brain around it, and around the solutions for it.</p>

<p>Here’s the interesting part: If you’ve ever edited a shared document, that odd
little sentence up there has affected you, too. What should the program do when
you and a collaborator both edit the same thing in different ways? Or if you,
working alone, edit the same document in different ways on different devices,
even offline?</p>

<p>Come with me on this journey into the interior of offline editing. I’ll keep
this lightweight. There’s no code discussed in here. This is all about exploring
possible solutions and showing why things are the way they are.</p>

<h1 id="when-edits-collide">When Edits Collide</h1>

<p>Our odd little sentence needs some collaborators. Say “hi” to Alice and Bob.
They’re both working on the same document that contains our sentence. Both Alice
and Bob think the word “owl” doesn’t really fit their overall theme, so they
choose a different animal.</p>

<p>Alice is a dog person, so she changes the word “owl” to “dog”. Bob is a cat
person, so he changes the word “owl” to “cat”. As it just so happens, they do
their changes at the same time, right down to the millisecond, one letter at a
time. And now the program has to make sense of it all.</p>

<p>What should the program do?</p>

<h1 id="working-in-an-always-online-world">Working In An Always-Online World</h1>

<p>Before we dive into anything else, let’s check out how the online world works.
You’ve already seen this sort of work happen, and it’s virtually guaranteed that
you’ve used it. Google Docs with a collaborator is a prime example of exactly
this sort of work. Both people online, editing the same document. Frequently,
there is also a voice call where the collaborators are talking through what
needs to be done.</p>

<p>The magic in this solution is multi-fold:</p>

<ul>
  <li>A central server acts as the referee. Essentially, Alice types her edit, Bob
types his edit, and the central server receives the edits and makes its
determinations about the output.</li>
  <li>Add in the voice call, and Alice and Bob both have a little laugh, settle the
age-old debate of cats vs dogs, and one choice gets made by the humans to
resolve the conflict.</li>
  <li>Without the voice call, you still get collaboration, it’s just a matter of
somebody making a choice and updating the edits again.</li>
</ul>

<p>You’ve already dealt with these exact sorts of conflicts before. Having the
central server referee the results is what happens, and humans are always kept
fully in the loop about what is happening in real time. That makes the entire
process easy to handle.</p>

<p>One document. One source of truth. Humans controlling every step of the way in
lockstep.</p>

<p>Unfortunately, this solution goes against the very nature of BrainFrame. Offline
first, local editing, and the lack of a server means that something else has to
happen. Fortunately, software developers have been dealing with variations of
this problem for decades, and have come up with many ways to work with it and
get something resolved. Going over all of those options would be worthy of a
book<sup id="fnref:nobook" role="doc-noteref"><a href="#fn:nobook" class="footnote" rel="footnote">1</a></sup>, so I won’t cover them in this post. I can promise that we have
been dealing with it since at least the 1960s<sup id="fnref:sccs" role="doc-noteref"><a href="#fn:sccs" class="footnote" rel="footnote">2</a></sup>. In one example that comes
to mind, a professor in college told the class I was in that one software
development group used paint sticks with file names on them. When a developer
wanted to update a file, they had to claim the paint stick. And then return the
paint stick when they were done. We have better solutions today 😀.</p>

<h1 id="when-locking-isnt-enough">When Locking Isn’t Enough</h1>

<p>That paint stick anecdote is an example of something called “pessimistic
locking”. On the surface it seems like a viable mechanism. Why not just prevent
somebody from editing the same file at the same time?</p>

<p>What happens when the edits <em>don’t</em> hit the same area? Alice is working on a
section talking about the colorful owl. Bob wants to work on a very different
section, and that section is just a table of contents for the whole document.
Alice has the stick, so Bob can’t make his edits even though there’s no reason
to prevent the edit. And what happens if Alice goes on vacation, or loses the
stick, or it gets broken somehow? Unless that stick gets replaced correctly,
nobody else can ever edit that file.</p>

<p>That sounds like it wouldn’t be a problem in today’s world, but what happens if
the paint stick is being held by a phone, and that phone gets dropped in a lake?
Without some work by somebody with technical skills, that file is locked
forever. Bob can never update the table of contents.</p>

<p>There is a variation called “optimistic locking” which addresses some of these
problems, but even it has limitations. For optimistic locking, the file would
get checked on save. The program basically asks “I’m about to overwrite this
file with new contents. Did somebody else update the same file while I was
editing? Or is everything good to go still?” The biggest problem with optimistic
locking is the need to have a second view of the new file. And somehow, the user
has to be able to tell the differences between the original file, the version
somebody else saved, and the new version of the file they are trying to save.</p>

<p>It’s a lot to ask of a human: Compare three things and figure out what changes
need to make it into the final results. Software development has a huge toolkit
built around this exact problem, and it’s still messy. For something like “The
colorful owl sat” it’s easy. But what happens when the changes are spread out
over a thousand lines? How can the human in the loop resolve this in a
meaningful way and <em>not</em> lose entire days just handling a simple line-by-line
comparison?</p>

<p>This is where a pattern is beginning to form: All the solutions sound easy,
right until they aren’t.</p>

<h1 id="when-diff-isnt-enough">When Diff Isn’t Enough</h1>

<p>Software developers reading along have already picked up on this next section
because they live it every day. We call these patterns “diff” and “merge”.</p>

<p>Broadly speaking, there are two kinds of changes: conflict-free and conflicted.
Using our example above, what happens when it’s only Alice editing the file? She
changes “owl” to “dog”, hits save, and that’s the end of the story. This is a
conflict-free change, and it’s extremely easy to manage. If Bob makes a change
to the table of contents, he’s not even editing the same section of the file. We
now have tools that make both of these changes conflict-free (for instance,
<a href="https://git-scm.com/">git</a>), so we software developers have, by and large,
abandoned using locks for writing our code. It’s safe to say that conflict-free
changes are almost boring at this point.</p>

<p>Conflicted changes are where it gets more interesting again. Go back to our
original example: Alice changes “owl” to “dog”. Bob changes “owl” to “cat”.
Unlike the real-time world of always-online, there’s no one to talk to offline.
In the world that BrainFrame lives in, that’s not viable. A human needs to sit
down, review the changes, and make the choice to determine what edit wins.
Entire tools and websites (like <a href="https://github.com/">GitHub</a>) exist to make
this process more manageable. As a developer, we go to the central source of
truth and make a choice. Once we have made that choice, all the other developers
have to grab that choice into their code.</p>

<p>The tools we have make it manageable, but they are decidedly <em>not</em> friendly
tools. Asking non-developers to master these tools is being unkind to them.</p>

<p>What happens, though, when there is no central server? What happens when Alice
and Bob make their edits at the same time, right down to the millisecond? It’s
not a good idea to ask the user to use tools like GitHub for what is free-form
prose (the heart of every note every user makes). It’s not a good idea to
present a separate note into their notes collection calling out the conflict.
The user has to spot it, and that is something very easy to miss.</p>

<p>The common thread for all of this is the decision: The user is always involved.
The computer can’t make the decision, because it doesn’t understand the <em>intent</em>
of the user. Without that understanding, it can’t do any resolution. And without
that resolution, the user has to step in and assert control over the process.
Sometimes, this is required. But most times, and especially for free-form text,
it really isn’t.</p>

<p>How can we let the computer do the work for us, and just tell us if/when we need
to be involved afterwards?</p>

<h1 id="something-old-and-something-new-crdts">Something Old and Something New: CRDTs</h1>

<p>After all of the above, you might be feeling hopeless. I know I did. I know what
I want, but I didn’t have anything to anchor it off of to actually <em>get</em> what I
want. Not until I found out about <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-free Replicated Data
Types</a>, or
CRDTs. These were first defined in 2011, and have been refined greatly in the
many years since then.<sup id="fnref:operational-transform" role="doc-noteref"><a href="#fn:operational-transform" class="footnote" rel="footnote">3</a></sup> The technology itself is old, but
for me, it is very new. I’ve only been learning it over the past couple of
weeks.</p>

<p>That mouthful of a name is a long-winded way of saying “The computer will always
figure out a merge path, and will not trouble the user with the need to deal
with merges.” In the next section, I’ll go over why this isn’t perfect, but for
now, for BrainFrame? CRDTs cover over 90% of all the cases for me very well.</p>

<p>Let’s build up what we’re doing, and how CRDTs handle them, one step at a time.
As I promised in the beginning, I’m not going to post code here. This is all
about what the results of using CRDTs are, and why they help.</p>

<h2 id="case-1-user-1-note-1-device">Case: 1 User, 1 Note, 1 Device</h2>

<p>Alice types up her note: “The colorful owl sat.” She saves it. Comes back a day
later, and changes “owl” to “dog”. For this case, we don’t need any special
mechanism. Your favorite text editor handles it without any stress to it or to
you.</p>

<h2 id="case-2-users-1-note-1-device">Case: 2 Users, 1 Note, 1 Device</h2>

<p>Alice and Bob share the laptop. Alice logs in, makes a change to the file. Bob
logs in, makes a different change to the same file. Again, nothing special here.
All of the cases so far are manageable with your favorite text editor.</p>

<h2 id="case-1-user-1-note-2-devices">Case: 1 User, 1 Note, 2 Devices</h2>

<p>A naive approach to this would say that there’s really no difference between
this and the first case. Alice can only edit in one location at a time, so the
changes are always going to be visible. That approach assumes an always-online
world, even without a centralized server. What happens if Alice’s phone runs out
of battery? If she had changed “owl” to “dog” on her phone beforehand, we have
an edit that her laptop doesn’t know about. She continues editing the original
document on her laptop, waiting for her phone to charge up. Once it does, she
turns it on, and the synchronization process begins. The “owl” becomes a “dog”
finally. But what if she changed her mind, and decided to turn the “owl” into a
“cat” while waiting for her phone to charge up? What happens then?</p>

<h2 id="case-2-users-1-note-2-devices">Case: 2 Users, 1 Note, 2 Devices</h2>

<p>Alice has a laptop, Bob has a phone. They are editing the same note, at the same
time, and staying in sync. There is no central server, the two devices have to
figure everything out and present the results to the user in a way that makes
sense. Possibly even more importantly, the two devices have to present the same
results to Alice and Bob without the devices “arguing” about which one of them
is right.</p>

<p>Remember: Alice changed “owl” to “dog”, and Bob changed “owl” to “cat”. How do
we decide on the result in a way that allows Alice and Bob to find and fix the
conflict?</p>

<h2 id="convergence">Convergence</h2>

<p>This is the heart of the problem. Everything else is a special condition on top.
How do two devices receiving concurrent updates from each other make sure
they’re giving the users the <em>same</em> result at the same time without some sort of
arbitration mechanism? How do they converge on a result they give to Alice and
Bob?</p>

<p>CRDTs do exactly this. I’m going to be a bit handwavy here, since I promised no
code. What CRDTs do, though, winds up feeling like magic, <em>especially</em> when
there’s no conflict. Alice changes “owl” to “dog”, Bob edits the table of
contents. Both devices synchronize. This is the most boring possible case for
all of it: The changes simply land where they are supposed to.</p>

<p>The whole process is transparent to Alice and Bob. No having to go to a central
source of truth and figure out the conflict and then make sure everybody gets
it. For Alice and Bob, the work is handled automatically by the computers, the
updates land, and everybody is happy.</p>

<p>The changes are anchored in the text in such a way that even if one of them
deletes a major section, as long as they don’t conflict with each other, all
changes will come out matching Alice and Bob’s intent. The document will remain
coherent and complete.</p>

<p>It might not be magic, but it certainly will feel like it. Documents get
synchronized across multiple devices with multiple users and everything
continues to make sense.</p>

<p>Or does it?</p>

<h1 id="crdts-arent-magic">CRDTs Aren’t Magic</h1>

<p>CRDTs provide a way for the computer to determine what should land, in what
order, when document edits collide. I cannot overstress the importance of this
enough: The mechanism <em>only</em> guarantees you a result; it does <em>not</em> guarantee a
meaningful result. For that, we still have to rely on the human in the loop.
What we can do, though, is make it easier than the tools that programmers have
to use. Prose doesn’t have to go through the same process that code does, so if
a mistake lands somewhere, the fix is easier to manage.</p>

<p>Make no mistake, though: Conflicts are a real problem, and not even CRDTs solve
them entirely. For that, let’s lean into our example from the beginning: “The
colorful owl sat.” Alice changed the word “owl” to “dog”, and Bob changed the
same word to “cat” at the exact same millisecond that Alice did. Above, I’ve
asked you to consider what happens, and how other systems try to solve it.</p>

<p>CRDTs solve it differently. Instead of picking a winner, CRDTs let <em>both</em> edits
win, simultaneously. All by themselves, CRDTs will produce a result that is
undeniably gibberish. “owl” will become something like “cdaotg”. Look through
that mess, and you’ll see the words “cat” and “dog” mixed together. The reason
for this is that additions to the note are prized above all else. If somebody
added something, then the result should have all of what they added. Since Alice
and Bob were typing at the same speed at the same time, their results got mixed
together at the same spot in the document. While the gibberish is, of course,
bad, it has one property that is good: It is <em>consistent</em>. Each device that
tries to view the note will get the <em>same</em> gibberish.</p>

<p>Have hope: All is not lost. This is a well-known problem, and it has a solution
that makes the gibberish more legible to humans. That solution is called
<a href="https://mattweidner.com/2022/10/21/basic-list-crdt.html">Fugue</a>. When we apply
that solution, the letters of the word stay together. The result of using Fugue
on these same edits will be either “catdog” or “dogcat” (there are some sorting
rules that come into play that I won’t go into for this post). While it’s
obviously wrong, it’s also something that us humans can reason through and fix.
Either Alice or Bob will make updates to the note, removing what they
collaboratively decide is the wrong word, and the note will make sense.</p>

<p>Where the hard part comes in is making sure that the conflicts are brought to
the attention of Alice and Bob. If they’re working with an entire collection of
notes, and these edits conflict, the application needs to notify them about the
conflict and direct them to its location. Then the human in the loop can fix the
problem.</p>

<p>There’s something even stranger hiding in our example: “The colorful owl sat.”
Alice decides to change the word colorful to the British spelling: “colourful”.
Bob decides to delete the line entirely. This is the most counterintuitive part
of CRDTs. Before I tell you the answer, can you guess it? Take a moment and
think about it. What will happen?</p>

<p>The answer will surprise you unless you’ve already spent time studying CRDTs.
The algorithms in this case are heavily focused on preserving every single
addition <em>after</em> all the deletions happen. If somebody took the time to add
something, we want to make sure that addition gets preserved. That’s why the
only thing left in that example is the single letter “u”. No, that’s not a
mistake. Alice added a letter. Bob deleted every letter in that line <em>except</em>
her addition. Alice’s newly added letter survives.</p>

<p>It might seem like this is a problem that the computer should be able to solve,
but it is actually impossible. The computer doesn’t know what Alice and Bob
<em>want</em>. It can’t discern intent from the text of the note. It can only direct
their attention to the conflict and let <em>them</em> decide what the correction should
be. Someday, we may be able to hook our brains up directly to the computers and
express our intent. If that day comes to pass, then the computer can solve it
for us.</p>

<p>That’s not the only cost of CRDTs. CRDTs have hidden bookkeeping costs. They
have to track every single change to every single character across every single
device. Those changes can’t be discarded until every device has checked in and
confirmed it has the updates for that note. If a device suddenly winds up at the
bottom of a lake, we have to provide additional housekeeping tasks that allow
Alice to tell the computer that a device is gone and won’t come back. Otherwise,
the collective changes for the note will grow forever. A single note that is
only a few kilobytes in length could have its change history occupying hundreds
of kilobytes (even megabytes) otherwise.</p>

<p>CRDTs make compromises, the same as every other solution we have. Those
compromises keep most of the cases running perfectly most of the time. As long
as we make sure that Alice and Bob can keep their eyes on enough information to
make those compromises acceptable, they work well.</p>

<p>Of course, this is all about text. Binary files (like graphics, PDFs, and ePubs)
are an entirely different story. And that story is part of BrainFrame’s overall
story, too.</p>

<h1 id="when-good-enough-is-enough-brainframes-bet">When “Good Enough” Is Enough: BrainFrame’s Bet</h1>

<p>We’ve covered a lot of ground here. We’ve got history, we’ve got software
development, we’ve even covered a few topics that most people outside of
software development have never even heard of (“pessimistic locking” anyone?).
All of this material is a way for me to communicate why I’m making the choices I
am for BrainFrame.</p>

<p>CRDTs are a tool that takes away most of the frustration of shared offline
editing with no central server. Unless you’re a developer, you’re not likely to
want to deal with locks and merges and conflict resolutions. Scratch that last
one: Not even developers want to deal with conflict resolution; it’s pretty
horrible. That doesn’t change the value of CRDTs.</p>

<p>For you, the goal is to provide a set of markdown files that you own, you edit,
you explore in your own way. To do that, and to provide the multi-device
experience that has come to be expected in the current age of computing,
BrainFrame has to do some background work. I’m already working on that, and I’m
making sure that the work is isolated from your notes. You will be able to use
any editor to make the files (yes, even Notepad/TextEdit/vi if you so choose),
and BrainFrame will do the work to make sure all your devices get it.</p>

<p>As was shown above, CRDTs aren’t magical, no matter how much we all wish they
were. Despite the ways in which they can miss, they provide escape hatches for
us. We can detect when things look like they weren’t handled smoothly. We can
make sure to bring those to the attention of the user of BrainFrame. We can
provide the right assistance, at the right time, to make sure that notes stay
coherent. It’s a big bet, but it’s one that I can (and will) deliver.</p>

<p>Another question some of you might be picking at: What about binary files? Their
answer is more mundane: The entire file has to be edited as a single unit. The
actual send between devices might be done in individual blocks and reassembled,
but the edit can never be at the individual character level. PDFs, ePubs, and
graphic files do not survive having their internals edited, and quite likely
will have their native editors and viewers simply put up an error message saying
that the file is corrupted. Sadly, we don’t have the magic for handling them
properly yet. That’s okay for us, though, since BrainFrame is about providing a
means to annotate and link to those files, not to make edits directly to them.</p>

<p>Other considerations do exist, and I’m not going over them in this article at
all. Things like “How do other devices actually <em>get</em> those changes?” are very
different questions, and are likely worthy of multiple articles.</p>

<h1 id="conclusion">Conclusion</h1>

<p>“The colorful owl sat.” Four innocuous words. Three changes, “dog”, “cat”, and
British spelling. Six words in total. And all of it turned my world upside down.</p>

<p>You don’t have to be a software developer for this to have turned your world
upside down either. You can see the problems. After this article, you can even
talk about them, maybe for the first time. And that is my true hope: Not that
I’ve turned your world upside down, but rather that I’ve given you something new
to think about. And if you <em>are</em> a software developer, perhaps even a new
problem to help solve for the world.</p>

<p>Go forth, and edit your notes. My job is to make sure those edits work. I’m on
it.</p>

<p>If you’d like to have a conversation with me about this article, you can find me
on Mastodon as <a href="https://c.im/@pedersen">@pedersen@c.im</a>. I look forward to it!</p>

<h1 id="links-and-references">Links and References</h1>

<ul>
  <li><a href="https://github.com/BrainFrameTech/playground">BrainFrame Code Playground</a> -
This is where I did my examination of CRDTs, but it will eventually include
other code snippets. It’s not production code, it’s all about learning the
behavior of tools that I am using.</li>
  <li><a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDTs</a></li>
  <li><a href="https://github.com/MattiaPispisa/crdt">Dart crdt library</a> - This is the CRDT
library being used by BrainFrame. Look under
<a href="https://github.com/MattiaPispisa/crdt/tree/main/packages/crdt_lf">packages/crdt_lf</a>
for the exact library source.</li>
  <li><a href="https://mattweidner.com/2022/10/21/basic-list-crdt.html">Fugue</a></li>
  <li><a href="https://git-scm.com/">git</a></li>
  <li><a href="https://github.com/">GitHub</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Operational_transformation">Operational
Transformation</a> - An
alternative technique that could have been used to replace CRDTs, but was not
chosen.</li>
  <li><a href="https://en.wikipedia.org/wiki/Google_Docs">Google Docs</a> - One of the most
famous products that <em>does</em> use Operational Transformation.</li>
</ul>

<h1 id="footnotes">Footnotes</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:nobook" role="doc-endnote">
      <p>Amusingly, this doesn’t seem to exist at this time. The history is
scattered across chapters, papers, and articles. The topic is still
book-worthy, and will have references to tools that are considered laughably
primitive today. I’m not the person to write it, though. BrainFrame demands
too much of my time for me to make an effective author for the topic. <a href="#fnref:nobook" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sccs" role="doc-endnote">
      <p>The first program to formalize this whole process was called
<a href="https://en.wikipedia.org/wiki/Source_Code_Control_System">SCCS</a>, and was
released in 1973. The problem itself is, of course, older. The code wouldn’t
have been written if the problem didn’t exist. Considering the first
multi-user operating system looks to be
<a href="https://en.wikipedia.org/wiki/Timeline_of_operating_systems#1960s">CTSS</a>
(released in 1961), it’s safe to assume the first conflicts were being felt
by users around the same time. <a href="#fnref:sccs" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:operational-transform" role="doc-endnote">
      <p>A similar technology known as “<a href="https://en.wikipedia.org/wiki/Operational_transformation">Operational
Transformation</a>”
takes a different approach. First defined in 1989, it was later adopted for
use in <a href="https://en.wikipedia.org/wiki/Google_Docs">Google Docs</a>. While there
is no actual <em>requirement</em> for using a central server to be successful with
Operational Transformation, the complexity involved in getting it right
(especially when CRDTs are much easier) makes this option a non-starter for
BrainFrame. <a href="#fnref:operational-transform" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="crdt" /><category term="sync" /><category term="offline" /><category term="conflictresolution" /><summary type="html"><![CDATA[Why concurrent edits are hard, and how CRDTs let offline devices agree on the same result without a central server.]]></summary></entry><entry><title type="html">Brainframe Idea - Imagine This</title><link href="https://brainframe.tech/blog/2025/09/17/brainframe-idea/" rel="alternate" type="text/html" title="Brainframe Idea - Imagine This" /><published>2025-09-17T01:20:00+00:00</published><updated>2025-09-17T01:20:00+00:00</updated><id>https://brainframe.tech/blog/2025/09/17/brainframe-idea</id><content type="html" xml:base="https://brainframe.tech/blog/2025/09/17/brainframe-idea/"><![CDATA[<p>Imagine an application that is a personal knowledge management system and that
maintains links to the original source material, regardless of format. The
source could be in PDF, EPUB, DOCX, Markdown, archived web pages, even Linux man
pages, and the application would internalize that source and maintain
cross-links from your notes to the original source.</p>

<p>Imagine being able to view your notes as standalone items as well as in the
context of the original source. You could review them and see where your
thoughts about a topic actually came from, and understand them better.</p>

<p>Imagine that application working across all three major desktop platforms
(Windows, Linux, and Mac), as well as the two major cell phone platforms
(Android and iOS). Imagine all your data being synchronized across them as
easily as you starting the application on a device.</p>

<p>Imagine all of this data being stored in as neutral a format as possible (your
notes in plain text, with the links to the original source being stored in a way
that would eanble you to find your way to the ideas without using this
application). Imagine your data beings yours, first and foremost, stored on your
devices rather than in the cloud.</p>

<p>Imagine the cloud being an <em>option</em> for storing your data.</p>

<p>Imagine this being available on a standalone device as well as all of the above.</p>

<p>All of these imaginings are what brainframe is meant to become. A reader for
these formats. A way to take the information on the go. A way for you to read,
to explore, to take notes, to connect ideas that were previously difficult to
connect.</p>

<p>Over the next few weeks, I’ll go over why the current applications people use
for this sort of work don’t cover everything. The applications are amazing, make
no mistake, but they are incomplete implementations of this idea.
<a href="https://obsidian.md">Obsidian</a>, <a href="https://www.notion.com/">Notion</a>,
<a href="https://roamresearch.com/">Roam</a>, <a href="https://orgmode.org/">OrgMode</a>, and others
all fill this idea to various degrees, but none of them implement it fully.</p>

<p>I am still teaching myself new technologies to help me make this. As a result,
the weekly posts I will make here will not be about the creation of Brainframe
directly, but rather about my thinking as I move forward with it. I hope you’ll
join me on this journey. It looks to be an interesting trip.</p>]]></content><author><name></name></author><category term="localfirst" /><category term="pkms" /><category term="sync" /><category term="vision" /><summary type="html"><![CDATA[The original vision for what BrainFrame could be.]]></summary></entry></feed>