v0.12.0 — P2P sync, daily-driver-ready

notes that don't
corrupt when you
sync them.

outl is a local-first outliner. Plain markdown is the source of truth. The CRDT lives in a sidecar, not in your files.
Two devices sync peer-to-peer to the same tree — end-to-end encrypted, no account, no cloud, no server that holds your notes. And your LLM reads them as a second brain over MCP.

0
formal guarantees
0
id:: in your .md
0
files benched
~/notes — outl May 26, 2026
journal · 🚀 launch-day ctrl-p ⌃k ?
synced 3 blocks 2 backlinks tree-crdt · op-log: 247
01 · the bet

two devices edit
offline.
they sync to the
same tree.

Roam keeps your notes on their servers — the later write silently wins. Logseq scatters id::abc123 UUIDs through your .md so rsync has something to match on; concurrent moves still lose data. git gives you conflict markers across nested bullets every time.

And every one of them round-trips your notes through a server you don't control — Roam's cloud, or the paid sync tier bolted onto the others. outl syncs peer-to-peer over iroh: your devices talk straight to each other, end-to-end encrypted. No account, no cloud, no server that holds — or reads — your notes.

outl uses the Kleppmann et al. 2022 tree CRDT — the algorithm that backs Automerge and Y.js, adapted for trees.

  • Strong eventual consistency. Same ops → same tree, any order.
  • Commutative after reordering. Late arrivals don't break the result.
  • Idempotent. Apply twice = apply once.
  • Tree invariant always holds. No node has two parents, no cycles.
  • No silent loss. Every op stays in the log — even ones turned into no-ops.
laptop A offline
laptop B offline
op-log step 0/5
private by default

your notes never touch
a server you don't own.

Roam and Notion keep everything on their servers. Logseq and Obsidian bolt on a paid cloud to move your files around. Either way, your notes ride through a company you don't control — one that can read them, leak them, or price you out.

outl syncs peer-to-peer over iroh. Your devices talk straight to each other, end-to-end encrypted — no account, no cloud, no third-party server in the middle. Privacy isn't a mode you switch on. It's the only way it works.

No server holds your notes.

Device-to-device over iroh, end-to-end encrypted with QUIC. Nothing we — or anyone — run ever stores your data. When your network blocks a direct path, a relay forwards bytes it can't decrypt; it never sees your notes.

Nothing to breach.

No account, no login, no paid cloud tier. There's no database of your notes sitting on someone's servers to leak — it doesn't exist. And the source is open, so you can audit the wire yourself instead of trusting us.

Zero UX tax.

Same TUI, desktop and iOS. Same clean markdown on disk. Offline-first — no spinner waiting on a server to answer. Pair two devices once with `outl peer pair` and they stay in lockstep. Privacy that isn't a downgrade.

02 · notes that compute

your code
your data
your notebook.

Markdown fences aren't just for syntax highlighting. Drop a ```python, ```lisp, ```js, ```lua or ```rust block. The result lands as a > result: subblock right underneath it. Re-runs are idempotent.

Set auto-run:: on a block and it re-runs every time you open the page — cache-aware by source hash. Adding a new language is an ~80-line adapter.

python
lisp
js
lua
rust
~/notes/algorithms.md ▷ run
title:: algorithms
fibonacci in 4 lines
```python
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)
print([fib(i) for i in range(10)])
```
idle
› result:
cached · hash: auto-run:: on
the result block is just a markdown blockquote — your file stays diff-able.
03 · markdown stays markdown

the markdown you see
is the markdown you wrote.

No id:: lines. No UUIDs. No HTML comments smuggling metadata. The IDs the CRDT needs live in a separate sidecar file (.foo.outl) — version it, or don't. Your .md is yours.

logseq · pages/post.md
- ## ship outl 0.1.0 id:: 6624a82c-3b11-4d44-9d3f-d9c7e8f0a1b3 collapsed:: false - draft post #launch id:: 6624a82c-9a22-4e55-be40-eaf8d9015c2a - ship binaries id:: 6624a82c-7c33-4f66-cf51-f0a9eb126d3b - linux x86_64 id:: 6624a82c-5d44-4077-d062-01baf1237e4c - macos arm64 id:: 6624a82c-3e55-411a-e173-12cbf2348f5d - test sync id:: 6624a82c-1f66-422b-f284-23dcf3459a6e - <!-- logseq.order-list-type:: number -->
outl · post.md + .post.outl (sidecar)
title:: ship outl 0.1.0 icon:: 🚀 tags:: launch - draft post #launch - ship binaries - linux x86_64 - macos arm64 - test sync
diff-friendly
your git diff is what you changed — not 30 UUID lines that shifted parents.
tool-friendly
works with pandoc, hugo, mdbook, your editor, your scripts. plain commonmark.
future-friendly
delete outl tomorrow. your notes still read the same in any text editor.
04 · second brain · new
just shipped · MCP server

outl is your
LLM's second brain.

The same notes you write in the TUI are the notes Claude, Cursor, Zed and ChatGPT read through the Model Context Protocol. One binary. One config block. No upload — your second brain stays on your disk.

  • read any page, search the graph, list backlinks, query by tag.
  • write new blocks, append to today's journal, toggle a TODO — from the chat.
  • pin outl://daily/today as a resource so the model has today's context every turn.
claude_desktop_config.json
{
  "mcpServers": {
    "outl": {
      "command": "outl",
      "args": ["--workspace", "~/notes", "mcp", "serve"]
    }
  }
}

MCP is an open standard. Any new host that adopts it picks outl up for free — the binary doesn't change.

04 · vs them

we like the ideas.
we don't like the trade-offs.

Roam, Logseq and Obsidian got a lot right — graph thinking, backlinks, daily journals, block addressability. outl picks those up and fixes the sync, the file format, and the runtime.

feature
outl MIT · rust
Roam closed · cloud
Logseq AGPL · electron
Obsidian closed · electron
markdown is the source of truth partial
no UUIDs in your .md n/a
block-level outliner plugin
[[wiki-links]] + backlinks
daily journal first-class
code blocks that execute 4 langs + 1 flag limited plugin
tree-CRDT algorithm (provably correct)
P2P device sync — no third-party server cloud paid paid
syncs on your Wi-Fi with no internet
works offline, no account
open source MIT AGPL
ships as a native binary
TUI / terminal editor
vim-style keys built-in plugin setting
desktop GUI app macOS beta
mobile app iOS + Android beta
graph view planned
plugin system JS · beta partial
shipped (outl) shipped (others) planned on the roadmap not supported
coming from logseq or roam?
$ outl import logseq ~/graph ~/notes
$ outl import roam ~/backup.json ~/notes

strips id::, resolves ((uid)) refs, slugifies filenames, seeds sidecars. unresolved refs stay as ((unresolved:UID)) for triage.

honest about what's not done

v0.12.0 ships the tree-CRDT algorithm + op-log infrastructure (validated by 10+ property tests), a daily-driver-ready TUI, and P2P sync via iroh as the default transport — your devices talk straight to each other, end-to-end encrypted, with no third-party server holding or reading your notes. Roam round-trips through their cloud; Logseq and Obsidian gate device sync behind a paid cloud tier. Code execution (four languages built in, a fifth behind a build flag) and a JavaScript plugin system both ship today. The pieces still marked planned above are graph view and the query DSL. macOS desktop is already shipping in beta via Homebrew cask (install here), iOS is on public TestFlight beta (join here) and Android ships an .apk on every beta release (download here). See the roadmap.

frequently asked

questions people actually ask.

What is outl?

+

outl is a local-first outliner — a bullet-point note-taking app where every line is a block you can move, reference and search. It stores plain markdown on disk (no UUIDs, no metadata pollution), has a tree-CRDT under the hood for sync that doesn't lose data, and ships as a single Rust binary with a vim-style TUI.

How is outl different from Logseq, Roam or Obsidian?

+

Three things. (1) Sync: outl syncs peer-to-peer over iroh — your devices talk straight to each other, end-to-end encrypted, with no third-party server holding or reading your notes. Roam round-trips through their cloud and loses data on offline conflicts, Logseq's built-in sync is rsync-flavored and overwrites, Obsidian's built-in sync is a paid cloud tier. Under the hood outl uses a Kleppmann tree-CRDT with five formal guarantees. (2) File format: Logseq writes UUIDs into your .md files. outl never does — IDs live in a sidecar. (3) Stack: outl is open source (MIT) and ships as a native binary; the others are Electron or proprietary.

Is outl open source and free?

+

Yes. outl is licensed under MIT and the entire codebase is on GitHub at github.com/outlmd/outl. There is no paid tier, no account, no telemetry. Free forever.

Does outl work offline?

+

outl is local-first. Your notes live as .md files on your machine — you edit them with zero network. No account, no cloud requirement. You can use outl on a plane and lose nothing; when two devices reconnect, the tree-CRDT merges both edit streams without dropping a block.

Do I need an account or a cloud to sync?

+

No. outl syncs peer-to-peer over iroh: your own devices connect straight to each other and the notes are end-to-end encrypted in transit. There's no account to create and no third-party server that holds, stores or reads your notes — they live only on your devices. (iroh uses relay servers to punch through NATs, but a relay only forwards already-encrypted bytes; it never stores or decrypts anything.) Compare: Roam and Notion keep your notes in their cloud; Logseq and Obsidian gate their built-in sync behind a paid cloud tier. Self-hosting over a shared folder you control is an advanced opt-in, but P2P is the default and needs no setup.

What does "local-first outliner" mean?

+

Local-first means your data lives on your device as plain files you own. The app reads and writes them. If outl disappears tomorrow, you can open every note in VS Code, cat, or any markdown editor — nothing is trapped. Outliner means the editor is built around nested bullets (blocks) instead of long prose, so you can collapse, reorder and link individual thoughts.

Can I import my notes from Logseq or Roam Research?

+

Yes. Run `outl import logseq <path-to-graph> <dst>` for Logseq, or `outl import roam <backup.json> <dst>` for Roam. The importer strips id:: lines, resolves ((uid)) block references, slugifies filenames and seeds the sidecars. Unresolved refs are kept as ((unresolved:UID)) for manual triage.

Does outl have backlinks and a graph view?

+

Backlinks are first-class — every page has a backlinks panel showing what references it. Wiki-style [[page name]] links and #tags work out of the box. A visual graph view is on the roadmap.

Can I run code inside my notes?

+

Yes — this is a core feature, not a plugin. Drop a fenced code block in Python, JavaScript, Lua or Rust and the result lands as a markdown blockquote subblock right under the source. Re-runs are idempotent and cache-aware (SHA-256 of source). Set `auto-run:: on` and it re-runs every time you open the page. Lisp exists too, behind the `lang-lisp` build flag rather than in the default build, because the Scheme engine offers no way to build a host boundary you can trust with code you did not write.

Can two devices sync without an internet connection?

+

Yes, if they are on the same network. outl discovers peers over mDNS (DNS-SD), so a laptop and a phone on the same Wi-Fi resolve each other's current address off the LAN itself and sync directly — no relay, no internet, no router configuration. It works on macOS, Linux, Windows, Android (which needs a multicast lock, because the Wi-Fi driver otherwise drops the packets below the socket API) and iOS (through Bonjour, so no special Apple entitlement is needed). If the LAN path is unavailable, sync falls back to the encrypted relay path exactly as before.

Is it safe to run a code block that came from another device?

+

A fence body is not always written by the person running it — it can arrive over sync from a paired device, through an import of someone else's graph, or from an LLM agent driving outl over MCP. So every runtime is bounded. Rust compiles to wasm and runs in wasmtime with no filesystem, no environment and no sockets. Python is built without its standard library and JavaScript with no host bindings, so neither has a file or a socket to reach for. Lua runs behind an explicit stdlib allowlist, with the os, io and package libraries removed. Every block also runs under a deadline, so a runaway loop cannot freeze your editor.

Can I use nested tags like #os/linux/debian?

+

Yes. A slash makes a real hierarchy: the parent page lists every descendant, at every level, and collects their mentions as backlinks of its own. The tree is derived from the page title rather than the filename, so os/linux still lives at a flat pages/os-linux.md and nothing on disk had to change — a graph imported from Roam years ago answers correctly without a migration. Matching compares slugified segments, so oscar/wilde is not in the os namespace even though it starts with those letters, and #os and #os/linux remain distinct tags.

What happens if I lose a device that had my notes?

+

Run `outl peer revoke-all` to empty the paired-device list, or `outl peer remove <device>` to drop one. Pairing is a grant and it is revocable. Your notes are on the devices themselves, not in a cloud account somebody else controls, so revoking membership is the whole of the operation — there is no vendor to email and no server-side copy to worry about.

Do I need a server to keep my devices in sync overnight?

+

No, and outl does not sell you one. Peer-to-peer is the default: your devices talk directly whenever both are awake. The gap is that peer-to-peer converges only when two devices can actually reach each other, and a laptop shut at 18:00 never overlaps a phone edited at 22:00. If that is your week, run `outl serve` on a machine you own — there is a Docker image for it. That box is one more peer holding a full replica, not a server your notes pass through. Turn it off and everything else carries on.

Is there a mobile app or desktop GUI?

+

Yes — both in beta. macOS desktop ships as a Homebrew cask: `brew tap outlmd/outl https://github.com/outlmd/outl && brew trust outlmd/outl && brew install --cask outl-desktop-beta` (universal dmg, Apple Silicon + Intel). iOS is on public TestFlight, and Android ships a sideloadable .apk on every beta release (grab the latest from the [download page](/download)). All frontends share the same outl-core and outl-md Rust crates, so the algorithm and file format don't fork.

How do I install outl?

+

On macOS or Linux: `brew tap outlmd/outl https://github.com/outlmd/outl && brew trust outlmd/outl && brew install outlmd/outl/outl-beta` for the TUI, CLI and MCP server, plus `brew install --cask outl-desktop-beta` for the GUI. `cargo install outl` builds from source. Then `outl init ~/notes && outl --workspace ~/notes` opens the TUI on today's journal. First-run cost is about thirty seconds. Stable `brew install outl` lands with 1.0.

everywhere you write

desktop, mobile, terminal —
same notes.

One workspace folder on disk. Three surfaces reading the same plain markdown — no proprietary database, no upload. Open it where the moment finds you. · click any screen to zoom

desktop · macOS
native macOS windows & linux too
mobile · iOS
iOS beta android apk on every release
terminal · TUI
macOS · linux · windows vim-first, mouse-optional
on the phone
a reminder you can answer

A banner used to tell you what was due and offer nothing to do about it — you opened the app, found the page, found the block. It carries Snooze 1h and Done now, and snoozing is an operation like any other, so it silences every device rather than the one you tapped. A plain tap opens the page and scrolls the block into view — scrolls, not zooms, because zoom hides the rest of the page and nobody asked for that by tapping a notification.

on the desktop
Open With → outl

Right-click a .md or .txt anywhere in Finder, Explorer or your file manager and it lands as a page, linked from today's journal so you can find it again without remembering it exists. outl imports a copy and never writes back to the file, and opening the same one twice takes you to the page the first open made instead of duplicating it.

on all three
a key that does nothing says so

Three surfaces will never do all the same things — a file manager has no terminal to hand a file to, and a phone has no chords. What each one can and cannot do is declared in code as an exhaustive list, so a client cannot ship a gap by forgetting to mention it. Press something that is not there and outl tells you, instead of leaving you to wonder whether it broke.

one workspace folder. every surface reads it. P2P CRDT sync keeps your devices in lockstep — talking straight to each other, no third-party server in the middle, and on the same Wi-Fi with no internet at all — even when you go offline.

05 · ship it

one binary.
your notes.

No electron. No account. No cloud. Press ▷ and you're typing in a journal.

homebrew recommended beta
$ brew tap outlmd/outl https://github.com/outlmd/outl
$ brew trust outlmd/outl
$ brew install outl-beta
cargo
$ cargo install outl
from source
$ git clone https://github.com/outlmd/outl && cd outl && cargo build --release
first run
$ outl init ~/notes && outl --workspace ~/notes

currently shipping outl-beta via the outlmd/outl tap. stable brew install outl arrives with 1.0. no brew? grab a binary for macOS, Linux or Windows →

stuck on something? ask in the discord.

real-time questions, ideas before they become issues, and the maintainer answering them. bugs still belong in an issue — everything else is a message.