AFFiNE Local-First Architecture: How CRDTs Enable Offline-First Collaboration
AFFiNE implements a local-first architecture by storing all user data locally using the y-octo CRDT engine and OctoBase embedded database, treating the cloud solely as a synchronization layer for conflict-free real-time collaboration.
AFFiNE is an open-source workspace application built around a strict local-first model where data lives primarily on the user's device. This architecture ensures users retain full data ownership while enabling seamless offline functionality and real-time collaboration. The implementation relies on three tightly-integrated components: a Rust-based CRDT engine called y-octo, an embedded storage layer named OctoBase, and a synchronization service that treats the cloud as an optional replica rather than the source of truth.
Core Components of the Local-First Stack
The CRDT Engine (y-octo)
At the heart of AFFiNE's local-first architecture is y-octo, a high-performance Rust implementation of conflict-free replicated data types (CRDTs). Located in packages/common/y-octo/core/, this engine provides the mathematical foundation that allows multiple replicas of a document to be edited concurrently without locking. Unlike the JavaScript-only Yjs library, y-octo offers native Rust encoding and decoding with lower memory overhead, making it suitable for resource-constrained environments.
Local Storage (OctoBase)
OctoBase serves as the embedded persistence layer, implemented as a lightweight Rust database that stores y-octo binary updates locally on disk. Referenced in the root Cargo.toml and detailed in the OctoBase repository, this component writes document states as self-contained binary blobs. Because the data is fully self-contained on the local filesystem (often SQLite-backed), the application remains fully functional without any network connection.
Sync Service (Backend Server)
The cloud layer in packages/backend/server/src/core/sync/sync.service.ts operates as a dumb pipe rather than an authority. It opens WebSocket channels to stream binary updates between devices, delegating all conflict resolution to the CRDT engine. The server in packages/backend/server/src/core/doc/writer.ts packs local y-octo updates into binary format for transmission, but never interprets or transforms the document content itself.
How Data Flows Through the System
The local-first workflow follows five distinct stages that ensure data integrity and availability:
-
Create a document locally – The front-end initializes a new Y-Doc using
new Doc()from the y-octo client and stores the document state as a series of binary updates. -
Persist the updates – OctoBase writes these binary updates to a local file or SQLite-backed store. Because the data is fully self-contained, the app works offline without any network connection.
-
Realtime collaboration – When online, the backend server opens a WebSocket channel. Each side streams y-octo updates; because CRDTs are commutative and idempotent, the order of arrival does not matter and merges are automatic.
-
Merge on conflict – y-octo’s built-in merging algorithm in
packages/common/y-octo/core/src/lib.rsresolves concurrent edits without user intervention, guaranteeing eventual consistency across all replicas. -
Sync to the cloud – The server persists a copy of every update in a central OctoBase instance, enabling other devices to pull the latest state when they reconnect.
Key Design Principles
This architecture fulfills three fundamental promises of local-first software:
-
Data ownership – All data is stored on the user’s disk first; the cloud never contains the authoritative copy. Users can export or migrate their data at any time because it exists as standard binary updates.
-
Offline-first operation – The UI in
packages/frontend/core/src/components/affine/editor.tsxreads from and writes to the local store exclusively. Network availability affects only synchronization, never functionality. -
Realtime collaboration – CRDT-based merges guarantee that changes made while offline are smoothly integrated when the device reconnects, with no manual conflict resolution required.
Implementation Deep Dive
Binary Update Format
The system converts all content into compact binary representations. In packages/backend/server/src/core/doc/writer.ts, the server converts Markdown input into y-octo binary updates. This binary format is what travels over the wire during WebSocket synchronization, minimizing bandwidth and ensuring deterministic merging.
Modular Architecture
The codebase separates concerns into independent crates and npm packages. The frontend (TypeScript/React), native Rust components (y-octo and OctoBase), and backend server (Node.js/Rust hybrid) are each replaceable units. This modularity allows developers to swap OctoBase with another embedded database or extend the CRDT engine without touching application logic.
Working with AFFiNE's Local-First APIs
Creating a Local Document (TypeScript)
When initializing content client-side, the application uses the y-octo client library to generate binary updates:
import { Doc } from '@y-octo/client'
// Initialise a new Y-Doc
const ydoc = new Doc()
// Add a shared text type
const ytext = ydoc.getText('content')
ytext.insert(0, 'Hello, AFFiNE!')
// Encode the current state as a binary update
const binary = ydoc.encodeStateAsUpdate()
// Persist locally (example uses IndexedDB via the helper library)
await window.indexedDB.storeUpdate('my-doc-id', binary)
This pattern appears throughout the frontend, ensuring every keystroke is captured as a CRDT operation before reaching any server.
Syncing Updates via WebSocket
When connectivity is available, the client streams binary diffs through a WebSocket connection:
const ws = new WebSocket('wss://sync.affine.pro')
// Send local updates
ws.addEventListener('open', () => {
ws.send(binary) // binary from previous step
})
// Receive remote updates
ws.addEventListener('message', ev => {
const remoteUpdate = new Uint8Array(ev.data)
ydoc.applyUpdate(remoteUpdate) // y-octo merges automatically
})
The applyUpdate method handles the mathematical merging of concurrent edits, ensuring that updates from other devices integrate seamlessly with local changes.
Converting Markdown to Binary (Rust)
On the server side, content transforms rely on Rust implementations for performance. The conversion logic resides in native code paths referenced from the backend:
use y_octocrate::{Doc, Update};
use crate::doc_parser::write::create::markdown_to_yocto;
/// Convert a markdown string into a binary y-octo update.
let markdown = "# Title\nSome **bold** text.";
let update: Update = markdown_to_yocto(markdown)?;
let binary: Vec<u8> = update.encode(); // ready for storage or transmission
This Rust-side processing in packages/common/native/src/doc_parser/write/create.rs ensures that heavy parsing operations do not block the JavaScript event loop.
Key Source Files
Understanding AFFiNE's local-first implementation requires familiarity with these specific locations:
| File | Role |
|---|---|
packages/common/y-octo/core/README.md |
Documentation for the CRDT engine used for local-first data |
packages/common/y-octo/core/src/lib.rs |
Core Rust implementation of y-octo CRDT primitives |
packages/common/native/src/doc_parser/write/create.rs |
Converts user-authored Markdown into y-octo binary updates |
packages/backend/server/src/core/doc/writer.ts |
Server-side writer that produces y-octo updates for storage |
packages/backend/server/src/core/sync/sync.service.ts |
WebSocket-based sync service distributing updates to clients |
packages/frontend/core/src/components/affine/editor.tsx |
Front-end editor mounting y-octo documents and rendering changes |
Cargo.toml (root) |
Declares y-octo and OctoBase crates powering local storage |
Summary
- AFFiNE stores all data locally first using the y-octo CRDT engine and OctoBase embedded database, ensuring users maintain data ownership.
- The cloud functions solely as a synchronization layer via WebSocket in
packages/backend/server/src/core/sync/sync.service.ts, not as an authority. - Offline-first operation is guaranteed because the UI reads from local storage in
packages/frontend/core/src/components/affine/editor.tsx, making network connectivity optional for functionality. - Conflict-free merging happens automatically through commutative, idempotent CRDT operations defined in
packages/common/y-octo/core/src/lib.rs. - Content transforms into binary updates via Rust implementations in
packages/common/native/src/doc_parser/write/create.rsfor efficient storage and transmission.
Frequently Asked Questions
What is local-first architecture?
Local-first architecture is a software design pattern where the primary copy of user data resides on the local device rather than a remote server. In AFFiNE, this means all document edits are first persisted to a local OctoBase database using y-octo CRDTs. The cloud exists only to synchronize these updates across devices, ensuring the application works offline by default and users retain full data ownership.
How does AFFiNE handle editing conflicts without a central server?
AFFiNE uses conflict-free replicated data types (CRDTs) implemented in the y-octo engine to handle concurrent edits. Because CRDT operations are mathematically guaranteed to be commutative and idempotent, edits from multiple users can be applied in any order without losing data. When devices reconnect, the applyUpdate method automatically merges divergent histories into a consistent state, achieving eventual consistency without manual intervention.
What is the difference between y-octo and Yjs?
While both implement the same CRDT algorithms, y-octo is a native Rust rewrite that provides faster encoding/decoding and lower memory usage compared to the JavaScript-based Yjs library. According to the source in packages/common/y-octo/core/, y-octo offers high-performance binary operations suitable for embedded storage, whereas Yjs requires a JavaScript runtime. AFFiNE uses y-octo specifically for its performance characteristics in resource-constrained desktop and mobile environments.
Can AFFiNE sync data after extended offline periods?
Yes, AFFiNE is designed for extended offline usage. While disconnected, the application continues writing y-octo updates to the local OctoBase store. When connectivity returns, the sync service in packages/backend/server/src/core/sync/sync.service.ts transmits the backlog of binary updates through WebSocket connections. The CRDT engine automatically reconciles these offline changes with remote edits, ensuring no data loss occurs regardless of how long the device was disconnected.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →