Build Reflect
A local notes app whose hard problems are links that survive a rename, search that answers while you type on ten thousand notes, and an export to plain files that does not mangle titles the operating system will not accept.
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 corpus and performance tests.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why Tauri 2 needs it, and the global shortcut and warm capture window live on that side.
Get it rustup.rs, then the platform build dependencies Tauri lists. open ↗
Verify
cargo --version prints a version and a dev build opens a window - have readyfree
Why Every performance claim in this build is meaningless on the twelve notes you wrote by hand, and the corpus is what makes the budgets falsifiable.
Get it Write the generator in phase three, with realistic note lengths, link density and title collisions.
Verify
The corpus loads and the search and backlink tests run against it - decidefree
Why Search response and shortcut-to-cursor are the two numbers this app lives or dies by, and choosing them after measuring is just describing what you built.
Get it Pick two numbers, write them in the documentation with the reason, and assert them in tests.
Verify
Both numbers appear in docs and in failing tests before the work starts
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- notes(id, title, body, created_at, updated_at, deleted_at, daily_date) · id is stable for the note's life and never derived from the title - aliases(note_id, alias, created_at) · every title a note has ever had, so an old link still resolves - links(source_id, target_id, target_text, resolved, ordinal) · resolved false means a link to a note that does not exist yet - tags(note_id, tag) · flat, because a hierarchy is a second feature pretending to be one - search_index(note_id, ...) · rebuilt on write, with the shape chosen after measuring on a real corpus - export_manifest(note_id, filename, title, exported_at, collision_suffix) · the mapping between ids and the names the filesystem allowed - The title is a label. Everything that refers to a note refers to its id, which is why renaming is free and export needs a manifest.
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 |
|---|---|---|---|
DATA_DIR | required | ~/.local/share/notes | The SQLite file and the search index. Back this up. |
EXPORT_DIR | required | ~/Documents/notes-export | Markdown files and the manifest. Overwritten on each export. |
CAPTURE_SHORTCUT | required | CmdOrCtrl+Shift+N | The global shortcut. Conflicts with another app are the commonest setup failure. |
SEARCH_BUDGET_MS | required | 80 | Asserted in the suite against the generated corpus. |
CAPTURE_BUDGET_MS | required | 400 | Shortcut to typable cursor, measured from a cold application. |
EXPORT_FILENAME_MODE | required | strict | Strict applies the Windows rules everywhere, so an export is portable between machines. |
The build, in order
Ids, not titles
A note keeps its identity through every rename, and old links still resolve.
Everything downstream depends on this one decision.
Files
src/notes/create.tssrc/db/schema.sqlRenaming a note should not orphan the sentence that mentioned it.
Files
src/notes/rename.tstests/rename-links.test.tsAmbiguous links are worse than rejected ones.
Files
src/notes/validate.ts
done when · tick each as it passeswatch out- Keying notes by title, as the file-per-note tools do, means renaming one either breaks every link pointing at it or forces a rewrite of every file that mentions it, and the copies you already exported keep the old name forever. Give each note an id, keep the old titles as aliases, and let the rename be a single row update.
Links and backlinks, computed once
Backlinks appear instantly and a link to a note that does not exist yet is a first-class thing.
One transaction, so a crash cannot leave half a graph.
Files
src/links/index.tsThe moment that makes the unresolved link useful rather than annoying.
Files
src/links/resolve.tstests/resolve-on-create.test.tsTen thousand notes, not the twelve you wrote by hand.
Files
tests/backlink-perf.test.ts
done when · tick each as it passeswatch out- Computing backlinks by scanning every note's body when a note is opened is fine at two hundred notes and unusable at five thousand, and because it degrades gradually you will not notice until the app you rely on daily has become slow. Write the links table on save and query it on read.
Search that answers while you type
A query over ten thousand notes returns ranked results inside a budget you wrote down first.
A number chosen after measurement is a description, not a target.
Files
tests/search-perf.test.tsdocs/budgets.mdTen thousand notes with real length distribution and link density.
Files
scripts/generate-corpus.tsNever let indexing be a reason to delay a save.
Files
src/search/index.ts
done when · tick each as it passeswatch out- Building the index on read, or rebuilding it wholesale on every save, makes typing in a large note stutter, and the reflex fix of debouncing the save is how people lose the paragraph they just wrote. Index incrementally on write, off the interaction path, and never trade a save for a search.
Capture in under a second
A thought can be written before it is lost, from anywhere, with no navigation.
The whole feature is the latency, and the latency is the warm window.
Files
src-tauri/src/capture.rsThe number that decides whether anyone uses this.
Files
tests/capture-latency.test.tsOne fewer decision between a thought and a record of it.
Files
src/capture/target.ts
done when · tick each as it passeswatch out- Making capture open the full application means a cold start, a window arranging itself, and a cursor that arrives second, which is long enough for the thought to go, so the feature quietly stops being used. Keep a warm capture window, focus the input before painting anything else, and measure the time from shortcut to typable cursor.
Markdown files the filesystem will acceptproduct builder
Every note exports as a plain Markdown file, on any operating system, without losing its identity.
Reserved characters, reserved device names, no trailing dot or space.
Files
src/export/filename.tstests/filename-rules.test.tsThe filename is a convenience; the id is the note.
Files
src/export/markdown.tsExport, wipe, import, compare ids, links and aliases.
Files
tests/export-roundtrip.test.ts
done when · tick each as it passeswatch out- Naming each exported file after its title works on your machine and breaks on a colleague's: a note called 'OS/2 vs NT' becomes a path with a directory in it, a note called 'NUL' cannot be created on Windows at all, and 'Meeting' and 'meeting' overwrite each other on a default macOS volume, so notes silently vanish from the export. Slugify against the strict rules, resolve collisions with a recorded suffix, and keep the true title in front matter where it cannot be mangled.
That is the whole plan for Reflect. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multi-device sync.
- Native mobile clients.
- Collaborative editing and sharing.
- Calendar integration.
- native mobile clients
- conflict-safe multi-device sync
- mature importers and exporters
- collaborative editing and sharing
- calendar-provider edge cases and timezone correctness
- A graph view, which needs nothing new because the links table already exists
- Transclusion of one note inside another, which is a render-time resolution of an id you already store
- A second export format such as a single archive file, reusing the manifest
Need the files? The project pack on the verdict page hands your agent the whole brief · more notes & knowledge.