Build Napkin

YESback to the verdict

0%0 of 17 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 local idea collection tool whose hard problems are capturing a thought without ever losing a keystroke, keeping a search index that can be thrown away and rebuilt from the files, and links that survive a rename.

estimated effort one sittingthe files for this build are in the project pack

ShellTauri 2 with React and TypeScriptSource of truthOne Markdown file per note in one folder, with front matter carrying the idIndexSQLite with FTS5 over an external content table, rebuildable by a scanLinksStable ids in front matter, titles resolved at render timeCaptureA global shortcut opening one text box that writes on a debounce and on blur

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 Tauri 2 needs both, and the global shortcut and file watcher come from the native side.

    Get it nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. open ↗

    Verify node --version prints v22 or higher and cargo --version succeeds

  2. installfree

    Why The whole search design rests on FTS5 and on external content tables, and a build without the extension fails in a confusing way.

    Get it Most distributions ship it enabled; confirm rather than assume. open ↗

    Verify Creating a virtual table using fts5 succeeds, and so does creating one with the trigram tokeniser

  3. have readyfree

    Why Every destructive test in this build runs against a real folder, and the copy is what lets you run them without fear.

    Get it Point the application at a folder you own, and keep a copy outside it before the first run.

    Verify A full scan indexes the copy and the counts match the file count

  4. have readyfree

    Why Search timing and rebuild duration only become real above a few thousand files, and that is exactly where a tokeniser choice starts to hurt.

    Get it A script that writes several thousand Markdown files with plausible text and links.

    Verify A full rebuild and a search both complete within your stated times

  5. decidefree

    Why Each is asserted by a test, and the tokeniser in particular is a trade-off that should be written down rather than inherited.

    Get it Write down the tokeniser per column with its reason, the capture write debounce, and how many folder snapshots are kept.

    Verify All three are in the configuration or the schema, and named in tests

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- notes(id, path, title, created_at, updated_at, mtime, size, content_hash) · a mirror of the folder, reproducible by a scan
- notes_fts(title, body) · FTS5 over the notes content table, with triggers keeping it in step
- links(from_note_id, to_note_id, raw_target, resolved, occurrences) · unresolved links are kept and shown, never dropped
- tags(note_id, tag) · parsed out of the text, which stays the only authority
- scan_state(last_full_scan_at, last_watch_event_at, files_seen, files_indexed, rebuild_ms)
- journal(seq, note_id, at, fragment) · appended as you type, replayed only after a crash
- Front matter carries the note id and its creation time. Everything else is derived, so deleting the database costs you a rebuild and nothing more.

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
NOTES_DIRrequired~/Documents/notesThe notes themselves. This is the thing to back up.
INDEX_PATHrequired~/.local/share/napkin/index.dbThe index. Safe to delete; it rebuilds from the folder.
CAPTURE_SHORTCUTrequiredCommandOrControl+Shift+SpaceThe global shortcut. Capture speed is the product.
WRITE_DEBOUNCE_MSrequired400How long after a keystroke the file is written atomically.
SNAPSHOT_DIRrequired~/.local/share/napkin/snapshotsFolder snapshots. The restore is exercised in the suite.
SNAPSHOT_KEEPrequired14How many snapshots to retain before the oldest is deleted.

The build, in order

  1. Capture that cannot lose the thought

    A global shortcut gives you a text box in under a second, and a crash mid-sentence loses nothing.

    1. The whole product lives or dies on this taking under a second.

      Files src-tauri/src/shortcut.rssrc/capture/Capture.tsx

    2. The journal is the thing that survives the crash.

      Files src/store/journal.tstests/journal.test.ts

    3. Temporary file, flush, rename. Never write in place.

      Files src/store/atomic.tstests/crash-replay.test.ts

    done when · tick each as it passes
    watch out
    • Writing the note only when the window closes loses the whole thought to a crash or a battery, and writing the real file on every keystroke leaves a half-written file when the process dies mid-write, which is worse because it looks like a note. Append to a journal, write the file atomically on a debounce, and replay the journal on start.
  2. The database is an index you can delete

    Deleting the database and restarting reproduces every note, tag and backlink from the folder.

    1. The folder is the truth and it can be edited elsewhere.

      Files src/index/scan.tssrc/index/watch.ts

    2. External content without triggers is an index that lies.

      Files src/index/schema.sqltests/fts-sync.test.ts

    3. Anything recoverable should be recoverable from a button.

      Files src/index/rebuild.ts

    done when · tick each as it passes
    watch out
    • An FTS5 external content table that is updated without the matching triggers goes quietly out of step with the table it indexes, and a search then returns rows whose content is NULL, which reads as an empty note rather than as a broken index (checked against the SQLite FTS5 documentation, September 2026). Write the three triggers, and make the documented rebuild command a button in the application rather than something you remember at three in the morning.
  3. Finding the note you half remember

    Search behaviour for short and partial words is a decision you made, not a surprise you discovered.

    1. Both options are defensible; the undocumented one is not.

      Files src/index/schema.sqldocs/search.md

    2. The shortest query is the one that surprises people.

      Files tests/short-query.test.ts

    3. Five thousand notes, generated, before you trust the timing.

      Files scripts/generate-notes.tstests/search-perf.test.ts

    done when · tick each as it passes
    watch out
    • Indexing with the default tokeniser means a search for a word buried inside a longer one never finds it, and switching everything to the trigram tokeniser fixes that while silently returning nothing for any query shorter than three characters and inflating the index (both checked against the SQLite FTS5 documentation, September 2026). Decide per column, document the choice, and put a test on the short query so the behaviour is visible.
  4. Links that survive a rename

    Renaming a note leaves every link to it working and every backlink intact.

    1. The id is the link; the title is the label.

      Files src/links/parse.tstests/rename.test.ts

    2. A broken link that silently disappears is a lost thought.

      Files src/links/unresolved.ts

    3. One source, so the two views cannot disagree.

      Files src/links/backlinks.ts

    done when · tick each as it passes
    watch out
    • Linking by filename or title means renaming one note breaks every link into it, and because the links still look like text nothing errors: you only find out months later when a backlinks panel is mysteriously empty. Link by id, resolve the title at render, and list unresolved links so a broken one is visible the same day.
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.