outl beta: three clients, one tree
TUI, native desktop, and an iOS app on TestFlight — all reading the same workspace folder. Apple-first, same Rust core, zero cloud. Inside: how outl makes file sync converge using a tree-CRDT and an HLC-ordered op log.
what’s shipping
Today the public beta opens. Three ways to use outl, all backed by the same Rust core (outl-core) and the same on-disk format — your plain .md files in a folder you own:
- TUI — the original. Vim-style modal editing, journal-first, fuzzy switcher, hot reload.
brew install outl@beta. - Desktop (macOS DMG) — Tauri shell, native menus, universal binary (Apple Silicon + Intel).
brew install --cask outl-desktop@beta. - iOS (TestFlight beta) — SwiftUI on top of
outl-coreviauniffiFFI. Read, edit and journal on the phone. testflight.apple.com/join/P2GdWAMd.
Apple-first isn’t an aesthetic choice. It’s the daily-driver setup I’m running: a MacBook, an iPhone, and a terminal that follows me everywhere. Linux desktop, Windows, and Android are next — same core, no rewrite. If you want the full download page, it auto-rebuilds against the latest beta tag on every deploy.
why three clients, not one
I’ve used Roam for years. I’ve used Logseq. I’ve used Obsidian. They each picked one surface and made you live there. Roam picked the browser. Logseq picked Electron. Obsidian picked a hybrid prose editor that pretends to be an outliner.
Where the actual work happens isn’t symmetric. I draft in the TUI because I think in keystrokes. I read on the phone because that’s what’s in my hand on the train. I open the desktop app when I want to drop a screenshot in or share my screen on a call. Three surfaces, three jobs, one workspace folder.
That only works if the data model is honest about it. There can’t be a “primary” client and “viewers.” Any client must be able to write, offline, and have the others catch up cleanly. That’s a sync problem — and sync is the part nobody talks about because it’s the part that breaks.
the part nobody talks about: sync
I wrote a long piece on this on my personal blog — short version below, but the long version is here: file sync isn’t trivial. Even Dropbox and Google Drive — products with hundreds of millions of users — still corrupt your tree on basic move operations in 2026. I tested it. They do.
The two scenarios that wreck a file sync are absurdly simple:
- Concurrent move with the same source. Two devices, both offline. Device A moves
/notes/Xinto/projects/. Device B moves the sameXinto/archive/. They come back online. Dropbox duplicates the node. Google Drive picks one by timestamp and silently drops the other. - Move into a cycle. Device A moves
B → A. Device B movesA → B. Now A is parent of B and B is parent of A. Dropbox duplicates. Google Drive freezes with “unknown error.”
These aren’t edge cases. They’re any outliner with offline editing. Drag a block, your phone drags a different block, you reconnect. If the sync layer doesn’t reason about tree invariants, you lose data — quietly.
how outl solves it
outl uses the tree-CRDT from Kleppmann, Mulligan, Gomes & Beresford (2022) — “A highly-available move operation for replicated trees” — formally proved in Isabelle/HOL. The algorithm fits in roughly 300 lines of Rust. The full walkthrough is at outl.app/sync; here’s the shape of it:
- Every edit is a move operation in an append-only op log. The tree you see on screen is just a projection of that log. The log is the source of truth.
- Operations are ordered by HLC timestamp (hybrid logical clock). No central server, no coordination — just a monotonic clock that’s globally orderable even with skewed wall clocks.
- Concurrent ops are reconciled by undo/redo into the log. When a remote op arrives out of order, we undo any later entries, slot the remote op into its timestamp position, redo. The mathematician’s word for this is commutativity: any permutation of ops yields the same final tree.
- Invariant: unique parent. Cycles are detected at apply-time and the offending op is dropped. No “B is parent of A and A is parent of B” ghost state. No duplicates as a workaround.
- Block text uses Yrs (Yjs in Rust) — character-level CRDT on the bullet content itself. Sibling order is fractional indexing so insertions between siblings don’t need rebalancing.
The five guarantees this gives you — convergence, no data loss, no cycles, intent preservation, eventual consistency — are backed by property tests on every cargo test run. Break one of them and the build is red. Read the manifesto.
what the beta actually does on Apple right now
The CRDT runs on every client. The op log is the single source of truth on each device. What’s not in this beta is the peer-to-peer network transport — iroh over QUIC, the next milestone on the public roadmap.
So how do the clients see each other today? iCloud Drive on the workspace folder. It’s a file-level sync, which we already established is unsafe in the general case — but here it doesn’t matter. The op log is append-only and the sidecar files are content-addressed. Two devices writing concurrently produce two op-log files that merge by the CRDT, not by iCloud. iCloud is just a delivery mechanism for opaque bytes; the convergence logic is all client-side.
When iroh lands, iCloud goes away. End-to-end encrypted P2P, no central server, no Apple in the middle. The algorithm doesn’t change — the wire does.
under the hood
One Rust core, three frontends, no duplicated business logic:
outl-core— CRDT, op log, parser, sidecar IO. Pure Rust. ~12k LOC.- TUI — Ratatui + crossterm. Talks to
outl-coreas a library. - Desktop — Tauri 2. Native macOS shell, web frontend for rendering,
outl-coreas a Rust dependency. Universal DMG (arm64 + x64). - iOS — SwiftUI app.
outl-corecompiled toaarch64-apple-ios+aarch64-apple-ios-sim, exposed via uniffi as an XCFramework. Same code paths the TUI hits.
If a sync edge case exists, it shows up in all three clients or none of them. That’s the point of putting the algorithm in the core and not in the UI.
try it
TUI
$ brew tap avelino/tap
$ brew install outl@beta
$ outl init ~/notes
$ outl --workspace ~/notes
Desktop
$ brew install --cask outl-desktop@beta
Or grab the universal DMG straight from GitHub Releases.
iOS
Join the TestFlight beta. Point it at your iCloud Drive folder (or any Files location) and the journal opens on today.
what people are saying
I posted the launch on a few places. The feedback so far has been the kind that actually moves the project — bug reports, “I want X next,” and the occasional “why are you doing this when Logseq exists” that I’m happy to answer.
- Reddit · r/PKMS — the long-form version, with the sync rant
- X · @avelinorun — the tight one-liner
- LinkedIn — for the corporate crowd
what’s next
- iroh transport — the wire that replaces iCloud as the delivery mechanism. E2E encrypted, P2P, ticket-based discovery.
- Android — Compose Multiplatform on the same
uniffisurface as iOS. - Linux + Windows desktop — Tauri build is already wired; needs CI signing and an icon refresh.
- Graph view — real backlinks panel + visual graph on desktop. The data is there, the rendering isn’t yet.
thanks
To everyone who installed the prerelease, broke it, and told me what was broken. To Martin Kleppmann for the paper that made any of this tractable. To the Yrs and iroh teams for the building blocks. To Conor and Tienson for proving the outliner could be the daily driver.
Code at github.com/avelino/outl. MIT. Issues open. Bug reports welcome — especially the ones that look like “I moved this block on my phone and now…”.
— Avelino