dotconferences
AI-assisted summary of dotJS 2019 - James Long - CRDTs for Mortals — original source. Check the original for full context.
Source published:
Key ideas
- Local-first applications require treating client devices as independent nodes in a distributed system, rendering naive offline request-queuing strategies ineffective under real-world conditions. (0:09)
- By modeling database tables as Grow-Only Sets of Last-Write-Wins Maps (LWW-Maps), developers can layer CRDT synchronization directly onto standard SQLite databases without sacrificing sub-millisecond local read performance. (16:03)
- A robust local-first synchronization engine does not require massive frameworks: James Long implemented Hybrid Logical Clocks in 256 lines of dependency-free JavaScript, while Actual's complete production client sync layer spans only 630 lines. (9:39)
- Manual conflict resolution hooks pushed to application developers inevitably fail; conflict freedom must be baked directly into the data structures and domain shapes themselves via commutativity and idempotence. (11:23)
Detailed notes
## Background Published on YouTube by dotconferences on January 9, 2020, this presentation was recorded at dotJS 2019 in Paris. According to the page description, speaker James Long explores the intersection of frontends and Conflict-Free Replicated Data Types (CRDTs), demonstrating how simple CRDT implementations deliver on the promise of robust, local-first applications with superior user experience.
## TL;DR James Long argues that reliable offline software requires treating client devices as distributed systems and adopting a local-first architecture powered by on-device storage like SQLite. To overcome the synchronization hurdles of unreliable ordering and concurrent edits, he introduces Hybrid Logical Clocks for causal ordering and CRDTs—specifically Last-Write-Wins Maps and Grow-Only Sets—to achieve automatic, deterministic convergence. By modeling database mutations as an append-only stream of cell-level messages and soft deletes, systems eliminate the need for error-prone manual conflict resolution. Demonstrating that his production sync engine in Actual Budget requires only a few hundred lines of dependency-free JavaScript, Long emphasizes that simplicity is the true prerequisite for distributed reliability.
## Atmosphere & style The talk carries an energetic, pragmatic, and candidly irreverent tone, playfully puncturing the intimidating academic jargon that usually surrounds distributed systems theory. Long speaks with the reassuring confidence of a builder who implemented these primitives in production, balancing technical rigor with lighthearted humor about developer habits and dependency bloat. Watching the talk feels both demystifying and empowering, turning an esoteric computer science concept into an approachable, elegant blueprint for everyday engineering.
## Bulletin - [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) Local-first applications require treating client devices as independent nodes in a distributed system, rendering naive offline request-queuing strategies ineffective under real-world conditions. - [16:03](https://www.youtube.com/watch?v=DEcwa68f-jY&t=963s) By modeling database tables as Grow-Only Sets of Last-Write-Wins Maps (LWW-Maps), developers can layer CRDT synchronization directly onto standard SQLite databases without sacrificing sub-millisecond local read performance. - [09:39](https://www.youtube.com/watch?v=DEcwa68f-jY&t=579s) A robust local-first synchronization engine does not require massive frameworks: James Long implemented Hybrid Logical Clocks in 256 lines of dependency-free JavaScript, while Actual's complete production client sync layer spans only 630 lines. - [11:23](https://www.youtube.com/watch?v=DEcwa68f-jY&t=683s) Manual conflict resolution hooks pushed to application developers inevitably fail; conflict freedom must be baked directly into the data structures and domain shapes themselves via commutativity and idempotence.
## Key takeaways - [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) Local-first architecture represents a complete paradigm shift over offline-first progressive enhancement, running full application code and data directly on the client machine. - [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) Queueing offline API calls into an array to push upon reconnecting fails because intermittent clients inherently form an asynchronous distributed system. - [02:12](https://www.youtube.com/watch?v=DEcwa68f-jY&t=132s) Keeping application data strictly client-side enables safe, open-ended ad-hoc queries—like Mongo-style queries compiled to raw SQLite—without exposing servers to injection or denial-of-service risks. - [02:12](https://www.youtube.com/watch?v=DEcwa68f-jY&t=132s) Synchronization engines should live as decoupled abstraction layers above standard relational databases rather than forcing developers to abandon battle-tested storage like SQLite. - [04:58](https://www.youtube.com/watch?v=DEcwa68f-jY&t=298s) Distributed synchronization faces two fundamental hurdles: unpredictable arrival ordering across peers and simultaneous write conflicts. - [04:58](https://www.youtube.com/watch?v=DEcwa68f-jY&t=298s) Offline-capable systems must abandon strong consistency models requiring synchronous network locks and instead embrace eventual consistency. - [07:25](https://www.youtube.com/watch?v=DEcwa68f-jY&t=445s) Physical device clocks cannot determine causal order across machines due to drift, while global incremental sequence IDs require unavailable central coordinators. - [09:39](https://www.youtube.com/watch?v=DEcwa68f-jY&t=579s) Hybrid Logical Clocks (HLCs) combine physical time and monotonic counters into compact strings that can be compared lexicographically to establish deterministic ordering across devices. - [09:39](https://www.youtube.com/watch?v=DEcwa68f-jY&t=579s) Simplicity is a prerequisite for distributed systems reliability, proven by a fully functional HLC implementation fitting into 256 lines of dependency-free JavaScript. - [11:23](https://www.youtube.com/watch?v=DEcwa68f-jY&t=683s) Delegating conflict resolution to user-defined hooks or manual callbacks fails due to the insurmountable edge cases of distributed state spaces. - [12:26](https://www.youtube.com/watch?v=DEcwa68f-jY&t=746s) CRDTs rely on two core mathematical properties—commutativity (order independence) and idempotence (duplication safety)—allowing clients to blindly broadcast changes until convergence. - [16:03](https://www.youtube.com/watch?v=DEcwa68f-jY&t=963s) SQLite can be converted into a distributed CRDT engine using an auxiliary `messages_crdt` table that tracks timestamp, dataset, row, column, and value for every atomic change. - [16:03](https://www.youtube.com/watch?v=DEcwa68f-jY&t=963s) Because Grow-Only Sets cannot physically delete records without creating distributed write anomalies, deletions must be modeled using tombstone flags. - [19:02](https://www.youtube.com/watch?v=DEcwa68f-jY&t=1142s) Intentional data modeling, such as dedicated mapping tables for category changes, eliminates merge conflicts before they can mechanically occur. - [19:02](https://www.youtube.com/watch?v=DEcwa68f-jY&t=1142s) A full production-grade synchronization engine can remain extremely lightweight, requiring only 630 lines of client JavaScript in Actual and 132 lines on the relay server.
## Detailed walkthrough ### [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) The Promise and Pain of Local-First Apps
James Long opens by asking why offline-first applications have never truly taken off across the software industry. The web fundamentally suffers from an inability to work without network access, and even when a network is present, high latency and patchy connections degrade the user experience into something that feels sluggish and unpleasant. The logical architectural response is to design software for offline operation from day one, which essentially requires building a fully local application where all code and application data reside directly on the client machine. Long emphasizes that he prefers the term “local-first apps” over offline-first, because local execution represents a complete architectural paradigm shift rather than just a progressive enhancement.
However, once an application keeps all of its data locally across multiple devices, synchronization becomes a massive, unavoidable hurdle. Many software teams attempt naive approaches that fall apart immediately under real-world conditions. When web applications attempt to paste offline capabilities onto traditional cloud architectures, users inevitably run into anxiety-inducing user interface errors when the network drops.
> “You cannot just queue up changes into an array when you are offline and then when you come back on online push them out to the server that does not work.”
To make genuine progress, developers have to shed these naive client-server mental models and confront what they are actually building.
> “The first thing we need to do is we have to admit that local apps they are a distributed system there's no way around that.”
- **The broken offline experience:** Mainstream web apps often flash warning banners stating, *“Attempting to restore a connection; changes may not be saved,”* leaving the user uncertain about their data’s fate. - **Distributed reality:** Every client running on an isolated phone, tablet, or desktop acts as an independent node that executes arbitrary operations before reconciling state with the rest of the network.
[02:12](https://www.youtube.com/watch?v=DEcwa68f-jY&t=132s) Building Actual Budget and Keeping SQLite
Long introduces his background as the creator of Prettier before discussing his project: Actual, a personal finance desktop and mobile app built around a local-first architecture. When designing Actual, the non-negotiable requirements were offline availability, blistering speed, strong user privacy, and support for arbitrary ad-hoc queries. Storing everything on-device immediately delivers extreme responsiveness without complex caching layers. Because user data is kept strictly client-side, the application can safely allow users to execute open-ended data explorations—featuring a MongoDB-like query syntax that compiles directly into raw SQLite—without exposing an application server to arbitrary code execution or injection exploits.
```mermaid flowchart TD accTitle: Local-First Application Architecture in Actual accDescr: Shows the relationship between client UI, SQLite storage, and the layered sync engine.
subgraph ClientDevice["Local Client (Desktop / Mobile)"] UI["Application UI / Custom Reports"] QueryEngine["Query Compiler (Mongo-like Syntax)"] DB[(Local SQLite DB)] SyncEngine["Sync Engine Layer"] end
RemoteServer["Minimal Relay / Storage Server"]
UI --> QueryEngine QueryEngine -->|"Sub-millisecond reads"| DB UI -->|"Local writes"| DB DB <--> SyncEngine SyncEngine <-->|"Propagates changes"| RemoteServer ```
To support entering transactions on the go, Long needed a mobile client that could synchronize with the desktop app. He was unwilling to adopt existing third-party syncing ecosystems because they forced developers to abandon standard relational databases and hand over control of their persistence layer. SQLite provided reliable, sub-millisecond local read queries that powered the entire UI, meaning any synchronization solution had to live strictly as an abstraction layer decoupled from the storage engine itself.
> “I did not want to give up SQLite; it gives me sub-millisecond read queries, which is absolutely amazing, it's the whole reason why the app is fast.”
- **Data scale:** Personal financial databases typically run between 5 and 10 megabytes, making multi-terabyte distributed database techniques overkill. - **Client safety:** Exposing raw querying capabilities like aggregate sums locally is completely safe, whereas cloud APIs like GraphQL strictly avoid arbitrary aggregates for security and denial-of-service reasons. - **Seamless synchronization:** Desktop and mobile clients must support concurrent updates, category creation, merges, and deletions, converging on the exact same state without data loss.
[04:58](https://www.youtube.com/watch?v=DEcwa68f-jY&t=298s) The Two Synchronization Hurdles: Unreliable Ordering and Conflicts
Building a custom synchronization engine brings developers face-to-face with two fundamental distributed systems problems: unreliable ordering and conflicts. In a local-first application, reliability must be absolute; users cannot simply refresh the browser tab to resolve broken states or recover corrupted data. When two independent clients mutate state over time, developers naturally want to imagine a single linear timeline where every change happened in a strict sequence. In practice, latency, physical network boundaries, and intermittent connectivity mean that events arrive at different peers in completely different sequences.
If Client A performs actions $A$, $C$, and $D$ while Client B performs action $B$, Client A might observe the stream of changes as $A \rightarrow C \rightarrow D \rightarrow B$, whereas Client B might receive and process them as $B \rightarrow A \rightarrow D \rightarrow C$. Naively executing state mutations upon receipt guarantees state divergence. While traditional backend systems rely on strong consistency models with intensive multi-node coordination, such mechanisms fail when devices are offline. Local-first architectures must instead embrace eventual consistency by accepting that concurrent timelines exist simultaneously.
> “A recent trend is to do eventual consistency, and this is a technique where you embrace the fact that there are multiple timelines, you embrace the fact that you are a distributed system, you admit that and then you find solutions that mesh well with that instead of trying to go against that.”
- **Strong consistency drawbacks:** Requires constant network roundtrips, locks, and strict global coordination—making it fundamentally incompatible with offline clients. - **The eventual consistency guarantee:** So long as all clients eventually observe the same set of operations, regardless of the order in which those operations are received, every client must deterministically converge on identical state.
[07:25](https://www.youtube.com/watch?v=DEcwa68f-jY&t=445s) Taming Time with Hybrid Logical Clocks
To solve unreliable ordering without a central coordinator, changes must be tagged with robust timestamps that establish causal relationships rather than physical clock time. Relying on physical system time via primitives like JavaScript's `Date.now()` is disastrous because device clocks drift, jump backwards, and cannot be trusted across disparate hardware. Furthermore, assigning an incremental integer sequence ID is impossible without a centralized authority granting numbers sequentially.
Instead, distributed systems use specialized logical timekeeping mechanisms. While vector clocks track causality by keeping an entry for every known peer, Long opts for a Hybrid Logical Clock (HLC). An HLC combines physical clock readings with a monotonically increasing logical counter into a single compact structure that can be easily serialized into a standard string. Running locally on each device without coordinating with a central server, an HLC produces timestamps that allow any syncing peer to compare two disparate mutations and determine their relative causal order.
> “All that we care about is when you assign a timestamp to a change, the clock is smart enough to give us—us being the ephemeral whoever is trying to sync some data—so that we can compare changes and say, ‘Did this change, when it happened on this machine, did it come after all of these other things that the machine has already seen?’”
- **Why physical clocks fail:** Client clocks fluctuate wildly across different hardware, meaning wall-clock timestamps cannot guarantee causal order or resolve concurrency. - **Why global sequence IDs fail:** Generating global sequential IDs ($1, 2, 3\dots$) requires strict real-time locking and network availability, which directly contradicts local-first operation. - **HLC advantages:** Encapsulates physical and logical progression, serializes into a comparable string representation, and operates independently on each device.
[09:39](https://www.youtube.com/watch?v=DEcwa68f-jY&t=579s) Hybrid Logical Clocks and the Value of Simplicity
James Long explains that when ordering operations, such as determining whether setting `x` to `3` wins over an earlier write in a last-write-wins strategy, systems rely on timestamps. However, developers should not be confused by timestamps that merely look like conventional dates. The underlying mechanism is a Hybrid Logical Clock (HLC). While there is an entire academic paper detailing the algorithm, the core practical concept is straightforward: an HLC produces a formatted string that can be compared lexicographically using simple standard operators (such as string `<` string) to reliably determine causality and order across devices.
Channeling Edsger W. Dijkstra's famous maxim that simplicity is a prerequisite for reliability, Long emphasizes that distributed systems are inherently intricate, but their solutions must not be. To build a system that achieves 100% reliability—surviving intermittent network conditions or recovering cleanly after a user leaves their phone in a drawer for two weeks—the codebase must remain as small and concise as possible. As proof, he showcases an implementation of Hybrid Logical Clocks that requires no external dependencies and fits in just 256 lines of JavaScript. He argues that developers frequently overcomplicate problems by trying to take on too much at once, rather than distilling the core challenge into a minimal, dependency-free implementation.
> “Dijkstra said simplicity is a prerequisite for reliability. This is a complex problem, distributed apps and distributed systems is a complex problem, and the way to have it reliable is to have a simple solution.” — James Long
- HLCs format timestamps into strings that are directly comparable to determine which event came first. - The presented HLC implementation is completely standalone: 0 dependencies and 256 lines of JavaScript. - Long cautions against over-engineering: “Dependencies suck,” so distilling the problem into a small, auditable footprint is vital.
---
[11:23](https://www.youtube.com/watch?v=DEcwa68f-jY&t=683s) Why Manual Conflict Resolution Fails
Establishing a reliable ordering mechanism still does not erase the reality of conflicting edits when two offline clients modify the same data field simultaneously before reconnecting. Long strongly criticizes traditional architectural approaches that delegate conflict resolution to user-written hooks or manual callbacks. In his view, pushing manual conflict resolution onto application developers is unfair and doomed to fail because the state space has far too many edge cases for even talented engineers to handle correctly.
Instead of treating conflict management as an afterthought or a cleanup step that runs after a sync cycle finishes, systems must handle conflicts inherently at the data modeling layer. Resolving conflicts must be baked directly into the data structures and shapes used by the application from the start.
> “Manual conflict resolution does not work; it's basically just issuing one of the hardest parts of this whole thing on to you, and as a developer, you are not gonna write correct manual conflict resolvers.” — James Long
---
[12:26](https://www.youtube.com/watch?v=DEcwa68f-jY&t=746s) Demystifying CRDTs: Commutativity and Idempotence
To solve synchronization without manual intervention, Long introduces Conflict-Free Replicated Data Types (CRDTs). He jokes about the academic jargon that often intimidates developers—parodying explanations that describe them as “a partially ordered monoid in the category of endofunctors with a least upper bound, so it's a semilattice essentially”—before assuring the audience that production systems require only a small fraction of the academic theory.
At their core, CRDTs are data structures characterized by two fundamental mathematical properties: commutativity and idempotence. Together, these properties grant a system extraordinary resilience under hostile network conditions, allowing clients to blindly broadcast updates until states converge.
```mermaid flowchart LR accTitle: CRDT Mathematical Properties accDescr: Diagram showing commutativity and idempotence leading to network resilience. subgraph Properties["Core Properties"] C["Commutative: Order does not matter"] I["Idempotent: Re-applying changes does not alter state"] end subgraph Outcome["System Result"] R["Superpower: Blindly send messages until states converge"] end C --> R I --> R ```
> “If it doesn't matter what order you apply changes, and it doesn't matter how many times you apply that change, you can essentially just throw changes over the wall, and if the state is not what you think it should be, then you just throw some more changes; I mean, it's essentially a superpower, right?” — James Long
- **Commutative**: Changes can be applied in any sequence and yield the identical state. - **Idempotent**: Applying the exact same change multiple times produces no additional side effects. - **Last-Write-Wins Map (LWW-Map)**: Demonstrated by setting values on keys (e.g., `x`, `y`, `z`). When a write of `x = 8` arrives after `x = 300` but bears an older HLC timestamp, the update is ignored. If updates arrive out of order, the higher timestamp ultimately overwrites the lower timestamp once received, converging on the exact same result regardless of receipt order. - **Grow-Only Set (G-Set)**: A collection where elements can only be added, never deleted. Because removing an item locally risks conflicts with incoming edits targeting that removed item, items in distributed sets can never be physically purged.
---
[16:03](https://www.youtube.com/watch?v=DEcwa68f-jY&t=963s) Turning SQLite into a Distributed CRDT Database
To translate these abstract concepts into real-world application storage, Long models a standard SQLite database table as a G-Set of LWW-Maps. He implements this mechanism using a single auxiliary table named `messages_crdt`. This table tracks every atomic change ever observed by the local node, regardless of whether the change was authored locally or synced from a remote peer.
```mermaid flowchart TD accTitle: SQLite CRDT Message Sync Flow accDescr: Flowchart illustrating how local and remote mutations pass through the messages table to update SQLite state. subgraph Input["Operations"] Local["Local Update / Delete"] Remote["Remote Sync Messages"] end subgraph SyncLayer["CRDT Sync Engine"] MsgTable["messages_crdt Table: dataset, row, column, value, timestamp"] end subgraph Storage["SQLite Target Tables"] CellUpdate["Update cell if timestamp > existing timestamp"] end Local --> MsgTable Remote --> MsgTable MsgTable --> CellUpdate ```
The `messages_crdt` table schema contains five primary fields: `timestamp` (the HLC timestamp), `dataset` (the target table name), `row` (the target row ID), `column` (the target column name), and `value` (the payload). As changes stream in, the engine updates the specific table cell if the incoming message's timestamp is newer than what currently resides in that position. If the row does not yet exist, SQLite inserts the row and populates that initial cell, gradually assembling the full record as subsequent column messages arrive.
- **Fast Local Reads**: Queries execute as raw, high-performance SQLite reads against local tables without any sync overhead during query execution. - **Unified Writes**: The application writes locally via helper functions such as `update(table, id, fields)`. Local mutations follow the identical code path as inbound sync events: both construct discrete message envelopes and process them through the syncing layer. - **Tombstones for Deletion**: Because G-Sets forbid physical deletion, a dedicated `delete(table, id)` function writes an update message setting a `tombstone` column to `1`. Read queries subsequently filter out tombstoned rows. - **Additional Capabilities**: Long notes that overall dataset consistency across clients can be validated using a Merkle tree (a tree of hashes tracking applied timestamps), and the entire architecture can be end-to-end encrypted across an untrusted sync relay server. - **Offline Category Demo**: Long demonstrates killing the sync server while continuing to add data offline, such as adding an item under a new category on one device while deleting that category on another. Through pre-mapped fallback logic (e.g., mapping obsolete categories into "Personal"), the clients smoothly reconcile their view hierarchies as messages sync upon reconnecting.
> “To change my local data, it's exactly the same way as syncing: when I get messages from other people, it syncs it through, it pushes it through the syncing layer; when I change stuff myself, I just call update, generates the messages, and it pushes it through the syncing layer—it's all the exact same thing.” — James Long
[19:02](https://www.youtube.com/watch?v=DEcwa68f-jY&t=1142s) Data Shaping and the Tiny Footprint of CRDTs
While conflict resolution often dominates discussions around distributed data, James Long explains that clever data modeling can prevent conflicts from happening in the first place. He notes that while he cannot explore every single technique in the talk, the core strategy is to intentionally shape your data model so that you “just bypass them altogether and that there doesn't exist a conflict.” In his own application, Actual, he uses approaches like a dedicated mapping table to handle categories automatically, ensuring that common conflicting states simply cannot arise mechanically.
```mermaid flowchart LR accTitle: Client-Server Sync Architecture accDescr: Diagram illustrating the minimal architecture and line counts of the sync implementation.
subgraph Client ["Client (Actual App)"] CL["Client Sync Layer<br/>(630 lines of JS)"] DB["Local Database Layer"] CL <--> DB end
subgraph Server ["Sync Server"] SRV["Message Relay Server<br/>(132 lines of JS)"] end
CL <-->|"Send / Receive Messages"| SRV ```
Beyond eliminating conflicts through structure, Long reveals just how lightweight a production-grade CRDT sync implementation can actually be. In his open-source sample repository, the entire client-side syncing layer and database logic fit into a mere 250 lines of JavaScript, while the backend server requires only 132 lines because its job is merely to receive messages and forward them back out. Even in his commercial personal finance product, Actual, the complete client sync layer totals only 630 lines of JavaScript. Putting that into perspective, he jokes that it amounts to about 65 tweets: “How long does it take you to tweet 65 times? Friday in bed tweet like 20 times so it's like three nights.” Crucially, this small footprint does not rely on massive, hidden third-party libraries. Its only dependencies are standard utilities for UUID generation and MurmurHash, avoiding the trap—referenced by speaker Christian Heilmann earlier in the conference—of stuffing complexity away into massive dependencies:
> “The only dependency is that it has our UUID and MurmurHash, right? You're not I'm not shoving garbage into the closet and then it's actually you're pulling in like a 150 megabyte dependency like Chris Heilmann said yesterday; this is the real code.”
- **Sample sync implementation:** 250 lines of client-side JavaScript (syncing + database) and 132 lines of server-side JavaScript. - **Production app (Actual):** 630 lines of client-side JavaScript for the entire synchronization engine. - **External dependencies:** Only `uuid` and `murmurhash`, ensuring no bloated dependency graph hides behind the low line count.
[20:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=1209s) The Local-First Future: Keep It Simple
Long wraps up by returning to the central motivation behind this architectural shift: delivering unmatched speed, resilience, and reliability to end users. Applications built local-first provide an inherently superior user experience because operations are instant and independent of network availability. He urges frontend and full-stack developers to demystify distributed systems concepts and embrace simple implementations of logical clocks and CRDT primitives instead of reaching for bloated frameworks or assuming offline sync is too complex for mortal engineers.
Long leaves the audience with a direct call to resist unnecessary software bloat:
> “We've got to start simplifying our solutions and not take on so much code which makes things more complex.”
## Notable quotes > [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) The first thing we need to do is we have to admit that local apps they are a distributed system there's no way around that. — James Long
> [11:23](https://www.youtube.com/watch?v=DEcwa68f-jY&t=683s) Manual conflict resolution does not work; it's basically just issuing one of the hardest parts of this whole thing on to you, and as a developer, you are not gonna write correct manual conflict resolvers. — James Long
> [00:09](https://www.youtube.com/watch?v=DEcwa68f-jY&t=9s) You cannot just queue up changes into an array when you are offline and then when you come back on online push them out to the server that does not work. — James Long
## Mentioned James Long, Prettier, Actual, SQLite, MongoDB, GraphQL, Edsger W. Dijkstra, Christian Heilmann, Hybrid Logical Clock, CRDT, Merkle tree, MurmurHash, UUID
## Video description Filmed at https://2019.dotjs.io on December 5-6, 2019 in Paris. More talks on https://dotconferences.com/talks
What do CRDTs and frontends have to do with each other? James talks about how CRDTs finally deliver on the promise of local-first apps, which provide superior user experience, and explains how simple CRDTs can be and how to leverage them to create robust local-first apps.
[Source](https://www.youtube.com/watch?v=DEcwa68f-jY)