Build Roam Research

YESreplaces $15/mosaves $180/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

ShellTauri with a TypeScript front endStorageOne Markdown file per note in a directory you chooseIndexSQLite with full text search, treated as a disposable cacheParsingAn incremental Markdown parser that extracts links and blocksWatchingA filesystem watcher with debounce

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
VAULT_PATHrequired~/NotesThe directory of Markdown files. This is your data and it belongs in your backup.
INDEX_PATHrequired~/.local/share/notes/index.dbThe search index. Deliberately outside the vault so syncing cannot corrupt it. Safe to delete.
WRITE_DEBOUNCE_MSrequired400How long to wait after typing before writing. Lower loses less on a crash and writes more often.
GRAPH_DEPTHrequired2How many link hops the graph view shows from the current note.
DAILY_NOTE_FORMATrequiredYYYY-MM-DDFilename format for daily notes. Changing it later does not rename existing notes.

The build, in order

  1. Files, watcher and the daily note

    Capture is instant, and everything you type lands in a file you could open in any editor.

    1. A directory chooser stored in settings. Everything else is relative to it.

      Files src-tauri/src/vault.ts

      terminal
      npm create tauri-app@latest
    2. 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.ts

    3. Record 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 passes
    watch 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.
  2. Links and backlinks

    Typing a link creates a connection, and the target note shows what points at it.

    1. 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.ts

    2. Resolve to a note at render time. This is what makes links to future notes work and renames cheap.

      Files src/links.ts

    3. Group 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 passes
    watch 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.
  3. Search that stays instant

    Search is fast enough to be the primary way you navigate, at ten thousand notes.

    1. Ten thousand notes with plausible link density. This fixture is what makes every later performance claim meaningful.

      Files scripts/generate-vault.ts

    2. Replace only the changed note's rows. A full reindex on every keystroke is the obvious implementation and the wrong one.

      Files src/index.ts

    3. A 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 passes
    watch 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.
  4. Block references and transclusion

    You can point at one paragraph rather than a whole note, and see it in place.

    1. 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.ts

    2. Look up by identifier and render the block's text with an affordance to jump to its source.

      Files src/transclude.ts

    3. A 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 passes
    watch 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.
  5. Graph, export and the honest limits

    You can see the shape of the vault, and leaving costs nothing.

    1. Nodes within a chosen link distance of the current note, not the entire vault.

      Files src/graph.ts

    2. Open it in a different Markdown editor and read several notes. This is the test that local-first is real rather than a slogan.

    3. 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 passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more notes & knowledge.