Under the hood

The technology behind Enliner

Enliner is a native macOS app written in Swift 6. The model layer is a standalone package that imports no UI framework. Your files stay canonical on disk as Markdown, a SQLite index serves the runtime, and every edit writes both. Here is the detailed version, subsystem by subsystem, with the numbers and the dependencies.

7,149
automated tests, across 611 files
≤ 16 ms
budget per keystroke, at 10,000 blocks
0
servers, accounts, or telemetry
100%
of the AI runs on your Mac

01 · Architecture

Three layers, one direction

The dependency arrow only ever points down: the SwiftUI app depends on the editor, the editor depends on the model, the model depends on nothing but the disk.

The model is a standalone package OutlineModel

Outliner (SwiftUI) → OutlineEditorOutlineModel → disk. The model layer imports no AppKit and no SwiftUI, so the whole editing engine is testable with no UI running. That split is the reason there are thousands of headless tests.

Files at rest, SQLite at runtime invariant

Our Markdown files are the source of truth on disk. A SQLite index is the source of truth while the app runs. Every edit writes both, and the index is fully rebuildable from the files. The index and other runtime state live outside the vault, in a device-local folder that never syncs.

State lives behind an actor Swift 6

Model state sits behind an actor Vault under strict concurrency. The editor sends a Command and gets back an immutable Diff; reads happen against an immutable Snapshot. Indexing and file I/O stay off the main thread.

Stable identity for every block ULID

Each block carries a 26-character Crockford base32 ULID, minted once and never rewritten. The generator is about forty hand-rolled lines with no dependency. Sibling order is a fractional key, so inserting between two blocks never renumbers the rest.

One Swift core, more platforms coming iPad · iPhone

The model and editor are plain Swift 6 with a single small platform seam for text input (NSTextInputClient on Mac, UITextInput on iOS). Most of the app carries over to iPad and iPhone as a theme swap on the same core, which is why those apps are what we build next.

02 · The editor

A caret engine, built from scratch

The editing surface does not use NSTextView. It is a custom Core Text engine, written to fix one specific defect: keeping the caret exactly where it belongs when the layout underneath it changes. Bike and TaskPaper left NSTextView for the same reason.

The caret is anchored to the model Core Text

The caret is a (BlockID, offset) pair, never a screen point. Relayout, reflow, and window resizing move pixels around it without moving it. Each block keeps a CTFramesetter cache keyed by its text, width, and theme generation.

Layout follows the viewport windowing

There is no character cap on a block. Layout is windowed to the viewport, so a block with hundreds of thousands of characters shapes only what we can see. Mapping a caret outside the window costs one line of shaping at most, never the whole block.

A 100,000-character block, edited live ~21 ms

A single giant block used to freeze for about 1,116 ms per keystroke. Incremental inline derivation, incremental layout repair, and a String.Index splice took that to roughly 21 ms median in a Release build. The splice itself is about 0.56 ms at 100k characters.

Japanese input is a build gate IME

CJK composition is a first-class case. Marked text renders while the model stays unmutated until the composition commits. A scripted NSTextInputClient corpus, IMEHarness, is a named gate that runs before the engine ships. The author works in Japanese daily.

The surface is pure SwiftUI now Canvas

The editor draws into a SwiftUI Canvas from a serializable list of draw commands replayed into a GraphicsContext. Core Text stays the typography layer. The one platform seam is a minimal TextInputProxyView for IME (NSTextInputClient on Mac, UITextInput on iOS).

Caret motion matches the platform geometry

Trailing newlines, blank lines, and up/down motion are checked against NSLayoutManager as the oracle. Both the linear and windowed geometry paths must agree, enforced by a differential caret-motion test matrix.

03 · Model & Markdown

One value type, and honest Markdown

Everything is a Block value type

A block holds an id, a parent, a fractional order, a RowType, its inline text, properties, and a collapsed flag. RowType covers plain rows, headings, bullets, numbered items, tasks, quotes, tables, and fenced code.

Mutation has one door Command / Diff

Nothing mutates a block in place. The editor holds a Snapshot, sends a Command such as splitBlock or indent, and applies the returned Diff. Undo coalesces contiguous edits to one block, and a full IME composition is a single undo group.

The source is the truth source-canonical

Inline styling is the Bike model: a plain string with tagged mark ranges, and no markup characters inside the display string. The stored text is the verbatim Markdown source, so what we wrote round-trips byte for byte.

The Markdown reader is hand-rolled MarkdownIO

A line-oriented reader and writer, built in-house for byte stability rather than a general parser. A property test generates random trees, with every row type, mark, soft break, CJK, emoji, and escape, and asserts that write, read, write is a fixed point.

Real constructs, not a subset == $ [!note]

Highlight with ==text==, callouts as a RowType.callout, reference links normalized to inline on paste, and math with $…$ rendered by SwiftMath (Core Text, so no WebKit in the editor). Pipe tables and fenced code are the two multi-line block types.

Syntax highlighting via tree-sitter SwiftTreeSitter

Code blocks tokenize through tree-sitter behind a SyntaxTokenizer protocol, with grammars pinned exactly (Swift, Python, JavaScript, JSON, Rust, Go, Bash). This one is built and gated on a later code-block phase, not yet switched on.

04 · On-device AI

The model runs on our machine

No cloud inference, no embedding API, no telemetry, no account. The whole pipeline works on a plane. We can watch the network stay silent while it runs.

A local LLM, loaded on demand MLX

Inference runs through Apple's MLX on Apple Silicon. The default model is Qwen3-4B at 4-bit, about 3 GB of RAM, with an 8B option on larger machines. ⌥⌘A loads and unloads it, RAM is freed when off, and it idles out after fifteen minutes.

Two transcription engines SpeechAnalyzer · WhisperKit

On macOS 26 the default is Apple's on-device speech recognition (SpeechAnalyzer), with no model download and faster-than-realtime output. WhisperKit (4-bit Core ML) is the alternative and the fallback, with explicit language detection for Japanese.

Speakers, the honest version Me / Them

Calls capture two tracks, so the split into us and the far end is structural rather than guessed: the mic is us, the far end is them. Full per-speaker diarization is designed as a drop-in seam and is not built yet. v1 ships the two-track ladder.

Semantic search with no vector database Qwen3-Embedding

Blocks embed to 1024 dimensions with a 0.6B on-device model. The vectors are fp16 BLOBs in a sibling SQLite file, searched by brute-force cosine through Accelerate's cblas_sgemv. That is 100% recall and a scan of a few milliseconds at 100k blocks, with no index to drift.

Ask the whole vault synthesis

A vault question expands into a handful of queries, gathers the best passage per page above a relevance floor, and runs a map-reduce over the results. The answer is ephemeral until we choose to turn it into a page.

Every AI edit is labelled ai::

Any block an AI wrote carries an ai:: property with its source and a UTC timestamp, and the editor shows a small gutter mark. We can always tell our own words from the machine's.

06 · Sync, sharing & the relay

Collaboration we can host ourselves

Sync is opt-in, per vault. A Local vault stays plain Markdown and never talks to a server. A Collaborative vault runs a CRDT, projects readable .md the whole time, and moves between machines through a relay that only ever sees ciphertext.

Enliner sync architecture Two paths. Personal sync moves plain Markdown files through iCloud Drive between one person's devices. Shared sync moves end-to-end-encrypted CRDT deltas through a content-blind relay between collaborators. Personal sync · one person, every device .md files .md files Your MacLocal vault iCloud Driveyour files, synced iPad · iPhoneplanned Shared sync · a few people, one vault ciphertext ciphertext YouCollaborative vault Relaysees only sealed bytes A collaboratortheir own Mac

Personal sync moves the vault's plain Markdown through iCloud Drive, with no relay and no CRDT. Shared sync runs a Loro CRDT and a content-blind relay that only ever forwards bytes sealed on your machine; host it yourself, down to a Raspberry Pi on your own network. The iPad and iPhone apps are on the way, built on the same Swift core.

A movable-tree CRDT Loro

Collaborative vaults use Loro, chosen for a first-class movable-tree type, which is exactly what an outline needs. A Collaborative vault keeps a snapshot plus an append-only log beside the files and projects readable Markdown the whole time.

A content-blind relay Rust · redb

The sync server is a single static Rust binary that forwards sealed bytes and never sees plaintext. It buffers each vault's deltas in an embedded redb file keyed by room and sequence, so a device that was offline catches up from the buffer with no other peer online. It authenticates who may sync; it cannot read what.

Admission is a signed challenge Ed25519

To connect, the relay sends a random nonce and we sign it with our Ed25519 identity key. It checks that key against the room's member list. Our private key stays in our Mac's Keychain and never crosses the wire.

Membership is a signed document epoch

The member list is a small document the owner signs: the room, an epoch counter, the owner, and each member's key and role. Only an owner or admin can change it, and only with a higher epoch. The relay verifies the signature over the exact bytes and enforces who may write, without understanding a word of the content.

The content key never reaches the relay X25519

Every delta is encrypted on our machine with the vault's content key before it is published. That key is wrapped to each member's X25519 public key, so adding someone means wrapping the key to them, not sharing a secret in the clear. The relay stores and forwards only what those keys have already sealed.

Inviting someone trust on first use

An invite is a one-time code we pass out of band. Devices pin each other's key on first use, and we can confirm a short fingerprint to be sure. Once a member is verified, they are challenged again only if their key changes.

Chunked, sealed deltas 4 MiB

Large CRDT deltas split into sealed chunks of about 4 MiB, one seal per logical delta, reassembled from ciphertext before a single decrypt. An early bug capped WebSocket messages below the relay's blob size and caused a resubscribe loop; the client limit now sits well above it.

Bounded and self-cleaning 256 MiB

The relay keeps a delta until every member has acknowledged it, then drops it, with a fourteen-day age-out and a 256 MiB per-room cap as backstops. An owner can mark a full-state snapshot so old history is reclaimed while the snapshot survives, and Stop Sharing wipes all server-side state for that vault at once. Its metrics report sizes and counts, never content.

Run it on our own network mDNS · wss

The relay advertises itself over mDNS as _enliner._tcp, so the app finds it on a flat network with nothing typed in. It terminates TLS itself with a self-signed certificate the app trusts by pinning the key's SPKI fingerprint. Install it as a hardened systemd service, keep the buffer on an SSD, and reach it from away over Tailscale rather than a forwarded port.

Verified across two Macs 435 MB

Live two-Mac realtime sync is verified, including a 435 MB, thirteen-thousand-block vault syncing between machines over the local relay. The robustness work beyond that is built and tested.

iCloud, the simpler option D28

A Local vault can sync as plain files through iCloud Drive, with no relay and no CRDT. The groundwork is done: runtime state like the index and caches was moved out of the vault into device-local storage, so only our canonical Markdown syncs and nothing machine-specific fights across devices. The file-sync transport on top of that is being finished.

Markdown survives, in every mode S7

A Collaborative vault keeps projecting plain .md, and moving a vault between Local and Collaborative preserves its ids. Whether we sync through the relay, through iCloud, or not at all, the vault still reads as files with no app in sight.

07 · Encryption at rest

Portable encryption, opt-in

Encryption is off by default and portable when we turn it on. The point is that an encrypted vault still opens with a standard tool and our key, with no app in the picture.

Content is sealed with age age

Markdown, assets, and snapshots are encrypted in the age format (X25519, ChaCha20-Poly1305, HKDF). The implementation is CryptoKit-native and wire-compatible with stock age, proven both directions at the spike.

Databases sealed whole ChaCha20-Poly1305

The runtime databases are sealed as whole files with ChaCha20-Poly1305, decrypted to the app container on unlock and re-sealed on lock. Full-database encryption and a stronger passphrase KDF are documented as later hardening.

One key, many ways to unlock Touch ID

A per-vault identity key is wrapped in a keyring to one or more unlock sources: a passphrase, the Secure Enclave through Touch ID, and a recovery seam. Adding or removing a way to unlock re-wraps the key only; our content is never re-encrypted.

We can always get back in recovery key

Turning on encryption forces us to export a recovery key and writes a plain DECRYPT.md with the exact commands to open the vault using stock tools. The model never sees ciphertext, and a crash mid-encrypt resumes cleanly.

08 · Durability & media

Built to outlive the app

The vault survives uninstall S7

Delete the app and the vault is still a folder of Markdown and assets we can read with anything. The runtime index is disposable and lives outside the vault, so nothing we care about depends on Enliner still being installed.

Web pages, captured whole single-file HTML

Saving a web page produces a self-contained .html with its CSS and media inlined as data URIs, stored content-addressed. Ten years on it opens with no server and no dead links, because there is nothing left to fetch.

Media is content-addressed sha256

Pasted and remote images are copied into the vault and named by their hash, which deduplicates identical files automatically. Remote images are fetched once and localized, so a link that dies upstream does not take our note with it.

09 · Graph view

A force graph, drawn natively

A pure-Swift engine GraphEngine

The graph is its own Swift package with the force simulation, a quadtree, and breadth-first search, and no UI imports. LogSeq's WebGL and d3 stack was deliberately avoided; there are no new dependencies here.

Sixty frames at two thousand nodes Canvas

The simulation runs off the main thread and hands a position buffer to a SwiftUI Canvas, which redraws each tick. The target is 60 fps at 2,000 nodes and 6,000 edges, with the main thread never blocked past a frame.

A ported force layout Barnes-Hut

The layout is a compact d3-force port: link distance, many-body repulsion with a Barnes-Hut approximation, collision, and centering. Nodes seed by a deterministic phyllotaxis pattern from a page-id hash, and the layout reheats when the index changes.

Pages are the nodes page-level

Nodes are pages, never individual blocks. Wikilinks, tags, and block references all resolve to page-level edges in one SQL pass, and node size scales by the cube root of degree so hubs read without swamping the view.

10 · Voice & calls

Recording that survives a crash

Both sides of the call process tap

Our voice comes from the mic; the far end comes from a Core Audio process tap. The two combine into an aggregate device with hardware drift compensation, so the tracks stay aligned to about 200 ms over an hour.

A recording that cannot be truncated to nothing CAF

The master is a native-rate 16-bit CAF written to be read to end-of-file, so a recording cut off by a crash is still a valid file. A manifest is written before the first audio buffer, tying stray files to the page we armed.

Recovery is idempotent recovery

Recordings stage in a non-purgeable recovery folder with an unflushed window under 100 ms. After a crash the app offers to recover, rebuilds the header from the manifest, and finalizes exactly as a clean call would. Running recovery twice is safe.

11 · Performance

Budgets we can fail a build against

Seven criteria, written down S1–S7

The spec fixes the numbers: open a 10,000-block vault within 1.5 s, answer a keystroke within one frame, move a row within a frame with no cursor jump, round-trip byte for byte, resolve backlinks within 100 ms, save a self-contained snapshot, and survive uninstall.

One source of truth for budgets PerfBudgets

The budgets live in one PerfBudgets type, and a headless harness plus a small perf-run command check the real numbers against them and print a pass-or-fail table. A regression is a failing build, not a feeling.

Measured on real hardware signposts

Latency is attributed with os_signpost on the running app and the wired path, not on isolated probes, which have mispredicted by threefold. The lesson is written into how the project works.

12 · How it is built

An agent pipeline, with receipts

Most of this code was written by AI agents running a supervised loop. The discipline around that loop is the reason it holds up.

Design, code, review, fix, verify pipeline

Every task runs the same loop, and the review is done by a different agent than the one that wrote the code, so the adversarial read is real. One task is one commit, prefixed with its phase.

Decisions are locked, and logged D0–D31

Thirty-plus locked decisions are not reopened in code. Anything that changes one lands as a dated entry in the decision log first. That log is public, below.

Direct, signed, and offline notarized .dmg

Enliner ships as a notarized, Developer-ID signed disk image, updated through Sparkle with signed packages. The license check is verified offline and never phones home at launch, and it gates the app, never our .md files.

Direct download, by design direct

There is no App Store build, because the payment path cannot coexist with the store's mandated in-app purchase. The distribution choice and the ownership model are the same decision.

The paper trail

Read the decision log

Every non-obvious choice in Enliner is written down the day it is made, with the reasoning and the tradeoff. Browse the full log: locked decisions, reversals, spikes, and the arguments behind them. Pricing and licensing entries are omitted from this public copy.