Build Roam Research
YESreplaces $15/mosaves $180/yrback to the verdict
A desktop notes application whose storage is a directory of Markdown files you can read without it: daily notes for capture, wiki-style links that build a backlink index, block references, full text search that stays instant at ten thousand notes, and an export that is simply the folder you already have.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Builds the front end and runs the tooling.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why Tauri compiles a native binary, and the filesystem watcher lives on that side.
Get it Install with rustup from rustup.rs. On Linux you will also need the platform packages Tauri lists for your distribution. open ↗
Verify
cargo --version prints a version and a Tauri hello world builds - decidefree
Why These are your notes in plain files. The application does not protect them; your backup does.
Get it Choose a directory covered by your existing backup, or set one up now. If you intend to sync it, decide between git and a file sync drive before Phase 6.
Verify
The chosen directory is included in a backup that has been restored at least once - installfree
Why Phase 5 verifies that the vault is readable without this application, which is the whole claim of local-first. You cannot test that with the application that wrote it.
Get it Any general Markdown editor you already have.
Verify
You can open the vault and read notes in it - have readyfree
Why Performance claims made against two hundred notes are meaningless, and the design that fails at scale feels fine until it does not.
Get it Write the generator in Phase 3 before optimising anything. Plausible link density matters more than realistic prose.
Verify
The generator produces ten thousand linked notes in a scratch directory
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- Files are the source of truth. Everything below is an index that can be deleted and rebuilt. - notes(id, path, title, mtime, size, content_hash) - blocks(id, note_id, ordinal, depth, text, block_uid) · block_uid is a stable identifier written into the file so references survive edits - links(from_note_id, to_title, from_block_id, kind) · kind is wikilink, tag or block_ref; to_title is resolved lazily so a link to a note that does not exist yet still works - notes_fts · a full text index over block text - A link to a non-existent note is normal and useful, not an error. Resolving it late is what makes capture fast.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
VAULT_PATH | required | ~/Notes | The directory of Markdown files. This is your data and it belongs in your backup. |
INDEX_PATH | required | ~/.local/share/notes/index.db | The search index. Deliberately outside the vault so syncing cannot corrupt it. Safe to delete. |
WRITE_DEBOUNCE_MS | required | 400 | How long to wait after typing before writing. Lower loses less on a crash and writes more often. |
GRAPH_DEPTH | required | 2 | How many link hops the graph view shows from the current note. |
DAILY_NOTE_FORMAT | required | YYYY-MM-DD | Filename format for daily notes. Changing it later does not rename existing notes. |
The build, in order
Files, watcher and the daily note
Capture is instant, and everything you type lands in a file you could open in any editor.
A directory chooser stored in settings. Everything else is relative to it.
Files
src-tauri/src/vault.tsterminalnpm create tauri-app@latest
Debounce keystrokes, then write to a temporary file and rename over the target. A partial write during a crash is how people lose a day of notes.
Files
src/storage.tsRecord the hash of every write. Ignore watcher events whose content hash matches what you just wrote.
Files
src/watcher.ts
done when · tick each as it passeswatch out- Your own writes will trigger your own file watcher and can produce a loop that reloads the note under the user's cursor. Track the hash you last wrote and ignore watcher events matching it, rather than trying to suppress the watcher around writes, which races.
Links and backlinks
Typing a link creates a connection, and the target note shows what points at it.
On save, reparse only the changed note and replace its rows in the links table. Full vault reparse belongs to index rebuild, not to editing.
Files
src/parse.tsResolve to a note at render time. This is what makes links to future notes work and renames cheap.
Files
src/links.tsGroup by source note, show the referring block plus its parent for context. A bare list of note titles is much less useful than it looks.
Files
src/backlinks.ts
done when · tick each as it passeswatch out- Resolving a link to a note identifier at parse time means every link to a not-yet-created note is either an error or a silently created empty file. Store the link target as text and resolve when rendering, which also makes renaming a note a cheap operation.
Search that stays instant
Search is fast enough to be the primary way you navigate, at ten thousand notes.
Ten thousand notes with plausible link density. This fixture is what makes every later performance claim meaningful.
Files
scripts/generate-vault.tsReplace only the changed note's rows. A full reindex on every keystroke is the obvious implementation and the wrong one.
Files
src/index.tsA menu item that deletes and rebuilds. Run it regularly during development so index corruption is never frightening.
Files
src/index.ts
done when · tick each as it passeswatch out- Test at the size you intend to reach, not the size you have. A vault of two hundred notes is fast with any approach, including a naive grep, and the design that fails at ten thousand feels perfect until the day it does not. Generate ten thousand notes and search them before you commit to the approach.
Block references and transclusion
You can point at one paragraph rather than a whole note, and see it in place.
Generate an identifier and append it to the source line in a form that reads acceptably in a plain text editor. Do not identify every block eagerly; most are never referenced.
Files
src/blocks.tsLook up by identifier and render the block's text with an affordance to jump to its source.
Files
src/transclude.tsA reference to a deleted block shows as broken, naming the identifier. Silence here produces documents with holes in them.
Files
src/transclude.ts
done when · tick each as it passeswatch out- A block identifier derived from position changes every time a line is inserted above it, silently repointing every reference. Write a generated identifier into the file the first time a block is referenced, and never derive identity from position or content.
Graph, export and the honest limits
You can see the shape of the vault, and leaving costs nothing.
Nodes within a chosen link distance of the current note, not the entire vault.
Files
src/graph.tsOpen it in a different Markdown editor and read several notes. This is the test that local-first is real rather than a slogan.
Block identifiers and any non-standard link syntax, so someone leaving knows exactly what to strip.
Files
README.md
done when · tick each as it passeswatch out- A force-directed graph of ten thousand nodes renders as a hairball and takes several seconds. Show a neighbourhood at a chosen depth from the current note. It is more useful and it is fast.
Sync, and being honest about conflictsproduct builder
Two machines can edit the vault, and a conflict is visible rather than silent.
Git gives history and explicit conflicts; a file sync drive is simpler and hides them. Pick one and say which in the README.
Files
README.mdWatch for the naming patterns sync tools produce and list them in the application as notes needing attention.
Files
src/conflicts.tsThe index must never be inside the synced directory, or two machines will fight over it and corrupt it.
Files
src/index.ts
done when · tick each as it passeswatch out- File sync tools resolve a simultaneous edit by keeping one version and, at best, leaving the other beside it with a mangled filename. Watch for conflict files, surface them in the application as something to resolve, and never assume the sync tool told the user anything.
That is the whole plan for Roam Research. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Conflict-free sync and real-time collaboration.
- The query language, which is a substantial feature in its own right.
- Mature importers from other notes tools.
- Sharing, publishing and collaborative editing.
- conflict-safe multi-device sync
- mature importers and exporters
- collaborative editing and sharing
- native mobile clients
- A simple query syntax over the links table, starting with all notes linking to two given notes
- Incremental parsing on keystroke rather than on save, once the save path is proven
- A publish command that renders a subset of the vault as static HTML
Need the files? The project pack on the verdict page hands your agent the whole brief · more notes & knowledge.