posts, no fluff.
Releases, design choices, and the occasional rant. Short list — only when there's something worth your time.
- · engineering
A keystroke should never wait for the disk
Adding backlinks made outl slow to open and edit on a big workspace: 2800 pages, 211k ops, and every Esc stuttered. The fix wasn't one trick, it was a rule. The op log is the truth, so everything else (the .md, the sidecar, the backlink index, the plugin hooks) is a projection that can happen in the background. How I found the real cost, the numbers before and after, and why the user should never feel any of the machinery.
read → - · engineering
The snapshot I couldn't reorder
I imported four years of notes out of another app in one shot, sixty-odd thousand blocks and a couple hundred thousand operations, and the volume dragged a pile of latent move-op and p2p-sync bugs into daylight in a single week. Boot got slow, so I did the obvious thing: cache the materialized tree and skip replaying the op log, and when a device pairs, ship it the tree the other device already built instead of a 200k-op log. Adopt the peer's snapshot, apply whatever it hasn't seen on top, done. It forked the tree. Two devices, the same set of operations, two different outlines, both internally consistent, both wrong about the other. The reason is the exact property that makes the move-op tree CRDT highly available: it converges by reordering the op log, undoing and redoing operations so a late one lands in its correct causal spot. A snapshot is materialized state with the log thrown away, so it's the one thing you can't reorder against, and a cycle-forming Move can resolve the opposite way on top of it. This is the one-line HLC guard that catches it, why the snapshot cutoff has to be a per-actor vector clock and not a global clock, the identical bug showing up again on the sync wire, and the storage door I'd left open that made every reload replay the whole log forever.
read → - · engineering
The status dot that broke sync
I wanted a green dot that said peer-to-peer was alive. The obvious way to know whether a peer answers is to dial it, so the status check stood up a small endpoint, dialed each peer, measured, tore it down. Thirty lines, read-only, can't hurt anything. It broke sync. Not the dot's report, sync itself: the relay keeps exactly one route per node id, my probe wore the device's identity, and registering stole the route from the endpoint doing the actual work. Inbound sync got CONNECTION_REFUSED from an endpoint that didn't speak the sync protocol. This is what iroh's source said, the nuance that turned 'never share the identity' into a better rule, and why the fix was to stop asking a question the transport had already answered.
read → - · engineering
Your notes never touch our relay
outl syncs your notes device to device with no server in the middle. Almost. There's exactly one server-shaped thing in the path, a relay, and it's the part that makes people nervous: 'wait, my notes go through a machine you run?' The honest answer is that the relay forwards bytes it can't read, and the reason it can't isn't a promise in a policy doc, it's the transport. This walks a single sync from boot to merge: the ed25519 identity that never leaves the device, the one QUIC endpoint, the vector-clock handshake that streams only the ops the other side is missing, and the exact moment the relay touches the connection. At every step the content is encrypted end to end with keys the relay never has. The step-by-step is the proof: there is no point in the pipeline where the relay could read a note even if it wanted to.
read → - · engineering
One node, two writers: how my split-brain fix doubled every title
Last month I gave every page root a deterministic id derived from its slug, so two devices that create the same day's journal land on the same node and merge instead of splitting. It worked. It also quietly doubled the title of every journal opened on two devices: '2026-06-25' became '2026-06-252026-06-25'. The Create op converged to one node exactly as designed, but each device also wrote the slug into that node's text CRDT, and two concurrent inserts at position zero don't overwrite, they concatenate. This is the story of two convergence mechanisms that are each correct and compose into a wrong answer, the two-replica test that proved it in eight lines, and why the fix was to stop using a sequence CRDT for a value that only ever has one writer's worth of meaning.
read → - · engineering
Two 'today' pages: debugging a bug I couldn't inspect
The journal on my phone flickered between two versions of the same day. I fixed the relay. Still flickering. I fixed a reload race in the frontend. Still flickering. I went to attach a debugger and the webview wouldn't let me. So I rendered the debug output onto the phone screen itself, and the numbers said the bug was in a layer I hadn't looked at once. This is the debugging story, the two wrong turns, the improvised instrument, and the split-brain in the op log it finally exposed.
read → - · engineering
The op log is the source of truth, so stop keeping everything resident
Importing 80,000 blocks killed outl on the iPhone: iOS jetsam shot the app on open because every block kept a live Yrs CRDT document resident for the whole session. The fix was to treat block text as what it already is, a projection of the op log, and rebuild it on demand. Half a gig of RAM down to a few megabytes, why the rebuild is provably exact, and the replay peak nobody thinks about.
read → - · privacy
Private by default: outl now syncs peer-to-peer, end-to-end
outl dropped iCloud for iroh. Your notes now sync device-to-device over end-to-end encrypted QUIC — no server, no account, no company in the middle. And you give up nothing: same TUI, desktop, and iOS, same clean markdown, same offline-first UX. Privacy that isn't a downgrade.
read → - · beta
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.
read → - · release
Announcing outl 0.1.0
outl 0.1.0 ships today: a local-first markdown outliner, vim-style TUI, executable code blocks in five languages, and the tree-CRDT algorithm that makes offline sync provably correct.
read →