Vibecode Napkin
track this build4 phases, 12 steps, beginner friendly0%Napkin's solo core is compact: build a local-first idea collection app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers.
You are building a lean indie version of Napkin. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Napkin · indie build 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 sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | capture has to work with no network, no sign-in and no waiting | | Source of truth | One Markdown file per note in one folder, with front matter carrying the id | the files are the notes; anything else is a convenience | | Index | SQLite with FTS5 over an external content table, rebuildable by a scan | an index you cannot delete is a second source of truth | | Links | Stable ids in front matter, titles resolved at render time | a renamed note must not break every link pointing at it | | Capture | A global shortcut opening one text box that writes on a debounce and on blur | if capture takes two seconds you stop using it and the product is over | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - 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. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **SQLite with FTS5 available** · free - 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. - Verify: Creating a virtual table using fts5 succeeds, and so does creating one with the trigram tokeniser - [ ] **A notes folder, and a copy of it** · free - 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 - [ ] **A few thousand generated notes** · free - 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 - [ ] **Your tokeniser, debounce and snapshot retention** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Napkin, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Napkin The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a capture-first notes app whose notes stay as plain Markdown files on your disk to replace Napkin. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript · capture has to work with no network, no sign-in and no waiting - Source of truth: One Markdown file per note in one folder, with front matter carrying the id · the files are the notes; anything else is a convenience - Index: SQLite with FTS5 over an external content table, rebuildable by a scan · an index you cannot delete is a second source of truth - Links: Stable ids in front matter, titles resolved at render time · a renamed note must not break every link pointing at it - Capture: A global shortcut opening one text box that writes on a debounce and on blur · if capture takes two seconds you stop using it and the product is over ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that cannot lose the thought Build: Register a global shortcut that opens one small window focused on an empty box. Append each change to a journal as you type. Write the note file atomically on a short debounce, on blur and on quit. On start, replay any journal entries newer than the file they belong to. Trap: 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. Done when: The capture window is focused and accepting input within a stated time from the shortcut, measured rather than felt. Killing the process mid-sentence and restarting recovers the text from the journal, tested by killing it. No note file is ever left partially written, tested by killing during a write. The journal is truncated once its entries are older than the file they wrote. Do not build yet: No search, links, tags or export. ### Phase 2 · The database is an index you can delete Build: Scan the folder into the index on start, then watch for changes. Keep the full-text index as an external content table over the notes rows, with triggers on insert, update and delete. Offer a rebuild command that drops and regenerates the whole index from the files. Trap: 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. Done when: Deleting the database file and restarting reproduces every note, tag and backlink identically, tested by comparing a full dump before and after. Editing a note file in another editor updates the index within a stated interval. The rebuild command completes and the timing is recorded, so a slow rebuild is visible before it becomes a problem. A search never returns a row whose content is missing, asserted by a test that deliberately desynchronises the index and then rebuilds. Do not build yet: No search interface, links or export yet. ### Phase 3 · Finding the note you half remember Build: Choose a tokeniser per indexed column and write down why. The default tokeniser gives you prefix matching and a small index; the trigram tokeniser gives substring matching and indexed pattern queries but matches nothing at all for a search under three characters, and costs much more space. Test the two-character query so its behaviour is stated rather than found. Trap: 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. Done when: The tokeniser choice per column is recorded in the schema file with its reason. A two-character query behaves as documented, asserted by a test rather than left to chance. A search for a substring inside a longer word returns the expected result for the columns where that was chosen. Search results return within a stated time on a folder of at least five thousand notes, measured with generated content. Do not build yet: No links, backlinks or export yet. ### Phase 4 · Links that survive a rename Build: Write links against the target's stable id and render the current title at display time. Keep unresolved links as rows rather than discarding them, and show them in one place so a typo is findable. Build the backlinks view from the same table. Trap: 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. Done when: Renaming a note leaves every inbound link working and every backlink present, tested by renaming a linked note. An unresolved link is stored and appears in a list of broken links rather than vanishing. The backlinks view and the links table agree, asserted directly. Links survive a full index rebuild, because they are parsed from the files. Do not build yet: No export or backup yet. ### Phase 5 · Already portable, and provably so Build: Import a folder of existing Markdown, assigning ids to files that lack them without rewriting their content otherwise. Export is a copy of the folder plus a manifest. Take automatic snapshots of the folder and exercise a restore in the suite. Handle a folder that a file sync tool has left conflicted copies in. Trap: Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. Done when: Importing a folder of plain Markdown assigns ids and leaves the body text byte identical apart from the front matter, tested. The export opens in any editor and the manifest lists every file with a hash. A restore from a snapshot is exercised in the suite rather than described. A conflicted copy left by a sync tool is surfaced as a conflict with both versions shown, never merged or silently imported as a new note. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a file copying one, and doing it badly loses notes. The folder is portable; leave the syncing to something that already does it. - Collaborative editing and sharing, which need an identity model this deliberately does not have. - A native mobile client, which is a second application rather than a feature of this one. - An importer for every other notes product. Write the one for the tool you are actually leaving, and make it exact. ### README must contain - That the Markdown files in the folder are the notes, and the database is an index that can be deleted and rebuilt - That capture appends to a journal and writes the file atomically, so a crash mid-sentence loses nothing - Which tokeniser each indexed column uses and what a query shorter than three characters does - That links are written against stable ids so a rename breaks nothing, and unresolved links are listed rather than dropped - That a file sync tool's conflicted copies are surfaced as conflicts and never merged ===== AGENTS.md ===== # Agent instructions · Napkin indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, One Markdown file per note in one folder, with front matter carrying the id, SQLite with FTS5 over an external content table, rebuildable by a scan, Stable ids in front matter, titles resolved at render time, A global shortcut opening one text box that writes on a debounce and on blur. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Napkin 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Open one box on a global shortcut, focused The whole product lives or dies on this taking under a second. Files: `src-tauri/src/shortcut.rs`, `src/capture/Capture.tsx` 2. Append to a journal as you type The journal is the thing that survives the crash. Files: `src/store/journal.ts`, `tests/journal.test.ts` 3. Write the file atomically and replay on start Temporary file, flush, rename. Never write in place. Files: `src/store/atomic.ts`, `tests/crash-replay.test.ts` ### Done when - [ ] Capture is focused within the stated time - [ ] A kill mid-sentence recovers the text - [ ] No partially written note file is ever left - [ ] The journal truncates once its entries are durable ### 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. ## Phase 2 · The database is an index you can delete Deleting the database and restarting reproduces every note, tag and backlink from the folder. ### Steps 1. Scan on start, watch afterwards The folder is the truth and it can be edited elsewhere. Files: `src/index/scan.ts`, `src/index/watch.ts` 2. Add the insert, update and delete triggers External content without triggers is an index that lies. Files: `src/index/schema.sql`, `tests/fts-sync.test.ts` 3. Make rebuild a first-class command Anything recoverable should be recoverable from a button. Files: `src/index/rebuild.ts` ### Done when - [ ] Deleting the database reproduces everything on restart - [ ] External edits reach the index - [ ] Rebuild completes and records its duration - [ ] No search result has missing content ### 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. ## Phase 3 · Finding the note you half remember Search behaviour for short and partial words is a decision you made, not a surprise you discovered. ### Steps 1. Choose the tokeniser per column and write down why Both options are defensible; the undocumented one is not. Files: `src/index/schema.sql`, `docs/search.md` 2. Test the two-character query The shortest query is the one that surprises people. Files: `tests/short-query.test.ts` 3. Measure on a realistic folder Five thousand notes, generated, before you trust the timing. Files: `scripts/generate-notes.ts`, `tests/search-perf.test.ts` ### Done when - [ ] The tokeniser choice and reason are recorded - [ ] A two-character query behaves as documented - [ ] Substring search works where it was chosen - [ ] Search is within the stated time at five thousand notes ### 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. ## Phase 4 · Links that survive a rename Renaming a note leaves every link to it working and every backlink intact. ### Steps 1. Link by id, render the title The id is the link; the title is the label. Files: `src/links/parse.ts`, `tests/rename.test.ts` 2. Keep unresolved links visible A broken link that silently disappears is a lost thought. Files: `src/links/unresolved.ts` 3. Build backlinks from the same table One source, so the two views cannot disagree. Files: `src/links/backlinks.ts` ### Done when - [ ] Renaming keeps inbound links and backlinks - [ ] Unresolved links are listed, not dropped - [ ] Backlinks and the links table agree - [ ] Links survive a rebuild ### 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. ## Not in this build - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. ## After v1, if you want it - A daily note that is just a note with a predictable name, once capture has been reliable for a month - Saved searches stored as files in the folder, so they travel with the notes - A graph view built from the links table, which already holds everything it needs ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The notes themselves. This is the thing to back up. NOTES_DIR=~/Documents/notes # Required. The index. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/napkin/index.db # Required. The global shortcut. Capture speed is the product. CAPTURE_SHORTCUT=CommandOrControl+Shift+Space # Required. How long after a keystroke the file is written atomically. WRITE_DEBOUNCE_MS=400 # Required. Folder snapshots. The restore is exercised in the suite. SNAPSHOT_DIR=~/.local/share/napkin/snapshots # Required. How many snapshots to retain before the oldest is deleted. SNAPSHOT_KEEP=14
You are building a lean indie version of Napkin. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Napkin · indie build 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 sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | capture has to work with no network, no sign-in and no waiting | | Source of truth | One Markdown file per note in one folder, with front matter carrying the id | the files are the notes; anything else is a convenience | | Index | SQLite with FTS5 over an external content table, rebuildable by a scan | an index you cannot delete is a second source of truth | | Links | Stable ids in front matter, titles resolved at render time | a renamed note must not break every link pointing at it | | Capture | A global shortcut opening one text box that writes on a debounce and on blur | if capture takes two seconds you stop using it and the product is over | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - 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. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **SQLite with FTS5 available** · free - 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. - Verify: Creating a virtual table using fts5 succeeds, and so does creating one with the trigram tokeniser - [ ] **A notes folder, and a copy of it** · free - 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 - [ ] **A few thousand generated notes** · free - 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 - [ ] **Your tokeniser, debounce and snapshot retention** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Napkin, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Napkin The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a capture-first notes app whose notes stay as plain Markdown files on your disk to replace Napkin. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript · capture has to work with no network, no sign-in and no waiting - Source of truth: One Markdown file per note in one folder, with front matter carrying the id · the files are the notes; anything else is a convenience - Index: SQLite with FTS5 over an external content table, rebuildable by a scan · an index you cannot delete is a second source of truth - Links: Stable ids in front matter, titles resolved at render time · a renamed note must not break every link pointing at it - Capture: A global shortcut opening one text box that writes on a debounce and on blur · if capture takes two seconds you stop using it and the product is over ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that cannot lose the thought Build: Register a global shortcut that opens one small window focused on an empty box. Append each change to a journal as you type. Write the note file atomically on a short debounce, on blur and on quit. On start, replay any journal entries newer than the file they belong to. Trap: 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. Done when: The capture window is focused and accepting input within a stated time from the shortcut, measured rather than felt. Killing the process mid-sentence and restarting recovers the text from the journal, tested by killing it. No note file is ever left partially written, tested by killing during a write. The journal is truncated once its entries are older than the file they wrote. Do not build yet: No search, links, tags or export. ### Phase 2 · The database is an index you can delete Build: Scan the folder into the index on start, then watch for changes. Keep the full-text index as an external content table over the notes rows, with triggers on insert, update and delete. Offer a rebuild command that drops and regenerates the whole index from the files. Trap: 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. Done when: Deleting the database file and restarting reproduces every note, tag and backlink identically, tested by comparing a full dump before and after. Editing a note file in another editor updates the index within a stated interval. The rebuild command completes and the timing is recorded, so a slow rebuild is visible before it becomes a problem. A search never returns a row whose content is missing, asserted by a test that deliberately desynchronises the index and then rebuilds. Do not build yet: No search interface, links or export yet. ### Phase 3 · Finding the note you half remember Build: Choose a tokeniser per indexed column and write down why. The default tokeniser gives you prefix matching and a small index; the trigram tokeniser gives substring matching and indexed pattern queries but matches nothing at all for a search under three characters, and costs much more space. Test the two-character query so its behaviour is stated rather than found. Trap: 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. Done when: The tokeniser choice per column is recorded in the schema file with its reason. A two-character query behaves as documented, asserted by a test rather than left to chance. A search for a substring inside a longer word returns the expected result for the columns where that was chosen. Search results return within a stated time on a folder of at least five thousand notes, measured with generated content. Do not build yet: No links, backlinks or export yet. ### Phase 4 · Links that survive a rename Build: Write links against the target's stable id and render the current title at display time. Keep unresolved links as rows rather than discarding them, and show them in one place so a typo is findable. Build the backlinks view from the same table. Trap: 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. Done when: Renaming a note leaves every inbound link working and every backlink present, tested by renaming a linked note. An unresolved link is stored and appears in a list of broken links rather than vanishing. The backlinks view and the links table agree, asserted directly. Links survive a full index rebuild, because they are parsed from the files. Do not build yet: No export or backup yet. ### Phase 5 · Already portable, and provably so Build: Import a folder of existing Markdown, assigning ids to files that lack them without rewriting their content otherwise. Export is a copy of the folder plus a manifest. Take automatic snapshots of the folder and exercise a restore in the suite. Handle a folder that a file sync tool has left conflicted copies in. Trap: Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. Done when: Importing a folder of plain Markdown assigns ids and leaves the body text byte identical apart from the front matter, tested. The export opens in any editor and the manifest lists every file with a hash. A restore from a snapshot is exercised in the suite rather than described. A conflicted copy left by a sync tool is surfaced as a conflict with both versions shown, never merged or silently imported as a new note. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a file copying one, and doing it badly loses notes. The folder is portable; leave the syncing to something that already does it. - Collaborative editing and sharing, which need an identity model this deliberately does not have. - A native mobile client, which is a second application rather than a feature of this one. - An importer for every other notes product. Write the one for the tool you are actually leaving, and make it exact. ### README must contain - That the Markdown files in the folder are the notes, and the database is an index that can be deleted and rebuilt - That capture appends to a journal and writes the file atomically, so a crash mid-sentence loses nothing - Which tokeniser each indexed column uses and what a query shorter than three characters does - That links are written against stable ids so a rename breaks nothing, and unresolved links are listed rather than dropped - That a file sync tool's conflicted copies are surfaced as conflicts and never merged ===== AGENTS.md ===== # Agent instructions · Napkin indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, One Markdown file per note in one folder, with front matter carrying the id, SQLite with FTS5 over an external content table, rebuildable by a scan, Stable ids in front matter, titles resolved at render time, A global shortcut opening one text box that writes on a debounce and on blur. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Napkin 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Open one box on a global shortcut, focused The whole product lives or dies on this taking under a second. Files: `src-tauri/src/shortcut.rs`, `src/capture/Capture.tsx` 2. Append to a journal as you type The journal is the thing that survives the crash. Files: `src/store/journal.ts`, `tests/journal.test.ts` 3. Write the file atomically and replay on start Temporary file, flush, rename. Never write in place. Files: `src/store/atomic.ts`, `tests/crash-replay.test.ts` ### Done when - [ ] Capture is focused within the stated time - [ ] A kill mid-sentence recovers the text - [ ] No partially written note file is ever left - [ ] The journal truncates once its entries are durable ### 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. ## Phase 2 · The database is an index you can delete Deleting the database and restarting reproduces every note, tag and backlink from the folder. ### Steps 1. Scan on start, watch afterwards The folder is the truth and it can be edited elsewhere. Files: `src/index/scan.ts`, `src/index/watch.ts` 2. Add the insert, update and delete triggers External content without triggers is an index that lies. Files: `src/index/schema.sql`, `tests/fts-sync.test.ts` 3. Make rebuild a first-class command Anything recoverable should be recoverable from a button. Files: `src/index/rebuild.ts` ### Done when - [ ] Deleting the database reproduces everything on restart - [ ] External edits reach the index - [ ] Rebuild completes and records its duration - [ ] No search result has missing content ### 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. ## Phase 3 · Finding the note you half remember Search behaviour for short and partial words is a decision you made, not a surprise you discovered. ### Steps 1. Choose the tokeniser per column and write down why Both options are defensible; the undocumented one is not. Files: `src/index/schema.sql`, `docs/search.md` 2. Test the two-character query The shortest query is the one that surprises people. Files: `tests/short-query.test.ts` 3. Measure on a realistic folder Five thousand notes, generated, before you trust the timing. Files: `scripts/generate-notes.ts`, `tests/search-perf.test.ts` ### Done when - [ ] The tokeniser choice and reason are recorded - [ ] A two-character query behaves as documented - [ ] Substring search works where it was chosen - [ ] Search is within the stated time at five thousand notes ### 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. ## Phase 4 · Links that survive a rename Renaming a note leaves every link to it working and every backlink intact. ### Steps 1. Link by id, render the title The id is the link; the title is the label. Files: `src/links/parse.ts`, `tests/rename.test.ts` 2. Keep unresolved links visible A broken link that silently disappears is a lost thought. Files: `src/links/unresolved.ts` 3. Build backlinks from the same table One source, so the two views cannot disagree. Files: `src/links/backlinks.ts` ### Done when - [ ] Renaming keeps inbound links and backlinks - [ ] Unresolved links are listed, not dropped - [ ] Backlinks and the links table agree - [ ] Links survive a rebuild ### 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. ## Not in this build - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. ## After v1, if you want it - A daily note that is just a note with a predictable name, once capture has been reliable for a month - Saved searches stored as files in the folder, so they travel with the notes - A graph view built from the links table, which already holds everything it needs ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The notes themselves. This is the thing to back up. NOTES_DIR=~/Documents/notes # Required. The index. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/napkin/index.db # Required. The global shortcut. Capture speed is the product. CAPTURE_SHORTCUT=CommandOrControl+Shift+Space # Required. How long after a keystroke the file is written atomically. WRITE_DEBOUNCE_MS=400 # Required. Folder snapshots. The restore is exercised in the suite. SNAPSHOT_DIR=~/.local/share/napkin/snapshots # Required. How many snapshots to retain before the oldest is deleted. SNAPSHOT_KEEP=14
You are building a production product version of Napkin. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Napkin · product brief ## Problem Napkin's solo core is compact: build a local-first idea collection app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers. ## Product outcome A notes application where capture is instant, the files are plain Markdown you could open in any editor, the database can be deleted without losing anything, and a rename never breaks a link. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients ## Success criteria - Capture is confirmed focused within the stated time - A kill mid-sentence is confirmed to recover the text - Deleting the index is confirmed to reproduce everything - A two-character query is confirmed to behave as documented - A rename is confirmed to leave backlinks intact ===== BRIEF.md ===== # Build brief · Napkin The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a capture-first notes app whose notes stay as plain Markdown files on your disk to replace Napkin. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript · capture has to work with no network, no sign-in and no waiting - Source of truth: One Markdown file per note in one folder, with front matter carrying the id · the files are the notes; anything else is a convenience - Index: SQLite with FTS5 over an external content table, rebuildable by a scan · an index you cannot delete is a second source of truth - Links: Stable ids in front matter, titles resolved at render time · a renamed note must not break every link pointing at it - Capture: A global shortcut opening one text box that writes on a debounce and on blur · if capture takes two seconds you stop using it and the product is over ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that cannot lose the thought Build: Register a global shortcut that opens one small window focused on an empty box. Append each change to a journal as you type. Write the note file atomically on a short debounce, on blur and on quit. On start, replay any journal entries newer than the file they belong to. Trap: 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. Done when: The capture window is focused and accepting input within a stated time from the shortcut, measured rather than felt. Killing the process mid-sentence and restarting recovers the text from the journal, tested by killing it. No note file is ever left partially written, tested by killing during a write. The journal is truncated once its entries are older than the file they wrote. Do not build yet: No search, links, tags or export. ### Phase 2 · The database is an index you can delete Build: Scan the folder into the index on start, then watch for changes. Keep the full-text index as an external content table over the notes rows, with triggers on insert, update and delete. Offer a rebuild command that drops and regenerates the whole index from the files. Trap: 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. Done when: Deleting the database file and restarting reproduces every note, tag and backlink identically, tested by comparing a full dump before and after. Editing a note file in another editor updates the index within a stated interval. The rebuild command completes and the timing is recorded, so a slow rebuild is visible before it becomes a problem. A search never returns a row whose content is missing, asserted by a test that deliberately desynchronises the index and then rebuilds. Do not build yet: No search interface, links or export yet. ### Phase 3 · Finding the note you half remember Build: Choose a tokeniser per indexed column and write down why. The default tokeniser gives you prefix matching and a small index; the trigram tokeniser gives substring matching and indexed pattern queries but matches nothing at all for a search under three characters, and costs much more space. Test the two-character query so its behaviour is stated rather than found. Trap: 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. Done when: The tokeniser choice per column is recorded in the schema file with its reason. A two-character query behaves as documented, asserted by a test rather than left to chance. A search for a substring inside a longer word returns the expected result for the columns where that was chosen. Search results return within a stated time on a folder of at least five thousand notes, measured with generated content. Do not build yet: No links, backlinks or export yet. ### Phase 4 · Links that survive a rename Build: Write links against the target's stable id and render the current title at display time. Keep unresolved links as rows rather than discarding them, and show them in one place so a typo is findable. Build the backlinks view from the same table. Trap: 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. Done when: Renaming a note leaves every inbound link working and every backlink present, tested by renaming a linked note. An unresolved link is stored and appears in a list of broken links rather than vanishing. The backlinks view and the links table agree, asserted directly. Links survive a full index rebuild, because they are parsed from the files. Do not build yet: No export or backup yet. ### Phase 5 · Already portable, and provably so Build: Import a folder of existing Markdown, assigning ids to files that lack them without rewriting their content otherwise. Export is a copy of the folder plus a manifest. Take automatic snapshots of the folder and exercise a restore in the suite. Handle a folder that a file sync tool has left conflicted copies in. Trap: Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. Done when: Importing a folder of plain Markdown assigns ids and leaves the body text byte identical apart from the front matter, tested. The export opens in any editor and the manifest lists every file with a hash. A restore from a snapshot is exercised in the suite rather than described. A conflicted copy left by a sync tool is surfaced as a conflict with both versions shown, never merged or silently imported as a new note. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a file copying one, and doing it badly loses notes. The folder is portable; leave the syncing to something that already does it. - Collaborative editing and sharing, which need an identity model this deliberately does not have. - A native mobile client, which is a second application rather than a feature of this one. - An importer for every other notes product. Write the one for the tool you are actually leaving, and make it exact. ### README must contain - That the Markdown files in the folder are the notes, and the database is an index that can be deleted and rebuilt - That capture appends to a journal and writes the file atomically, so a crash mid-sentence loses nothing - Which tokeniser each indexed column uses and what a query shorter than three characters does - That links are written against stable ids so a rename breaks nothing, and unresolved links are listed rather than dropped - That a file sync tool's conflicted copies are surfaced as conflicts and never merged ===== ARCHITECTURE.md ===== # Architecture · Napkin ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | capture has to work with no network, no sign-in and no waiting | | Source of truth | One Markdown file per note in one folder, with front matter carrying the id | the files are the notes; anything else is a convenience | | Index | SQLite with FTS5 over an external content table, rebuildable by a scan | an index you cannot delete is a second source of truth | | Links | Stable ids in front matter, titles resolved at render time | a renamed note must not break every link pointing at it | | Capture | A global shortcut opening one text box that writes on a debounce and on blur | if capture takes two seconds you stop using it and the product is over | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | The shortcut, the journal and atomic writes. | Any front end, provided the journal and the atomic write stay. | | Index | Scan, watch, triggers and rebuild. | Any index, provided it is rebuildable from the folder alone. | | Search | Tokeniser choice and query behaviour. | Either tokeniser, provided the choice and the short-query behaviour are documented. | | Links | Id-based links, backlinks and unresolved links. | Not swappable for filename links, which break on rename. | | Portability | Import, export, snapshots and conflict detection. | Any snapshot tool whose restore is exercised in the suite. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `NOTES_DIR` · required · The notes themselves. This is the thing to back up. - `INDEX_PATH` · required · The index. Safe to delete; it rebuilds from the folder. - `CAPTURE_SHORTCUT` · required · The global shortcut. Capture speed is the product. - `WRITE_DEBOUNCE_MS` · required · How long after a keystroke the file is written atomically. - `SNAPSHOT_DIR` · required · Folder snapshots. The restore is exercised in the suite. - `SNAPSHOT_KEEP` · required · How many snapshots to retain before the oldest is deleted. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Napkin product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, One Markdown file per note in one folder, with front matter carrying the id, SQLite with FTS5 over an external content table, rebuildable by a scan, Stable ids in front matter, titles resolved at render time, A global shortcut opening one text box that writes on a debounce and on blur. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. ===== MILESTONES.md ===== # Delivery milestones · Napkin Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Open one box on a global shortcut, focused The whole product lives or dies on this taking under a second. Files: `src-tauri/src/shortcut.rs`, `src/capture/Capture.tsx` 2. Append to a journal as you type The journal is the thing that survives the crash. Files: `src/store/journal.ts`, `tests/journal.test.ts` 3. Write the file atomically and replay on start Temporary file, flush, rename. Never write in place. Files: `src/store/atomic.ts`, `tests/crash-replay.test.ts` ### Done when - [ ] Capture is focused within the stated time - [ ] A kill mid-sentence recovers the text - [ ] No partially written note file is ever left - [ ] The journal truncates once its entries are durable ### 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. ## M2 · The database is an index you can delete Deleting the database and restarting reproduces every note, tag and backlink from the folder. ### Steps 1. Scan on start, watch afterwards The folder is the truth and it can be edited elsewhere. Files: `src/index/scan.ts`, `src/index/watch.ts` 2. Add the insert, update and delete triggers External content without triggers is an index that lies. Files: `src/index/schema.sql`, `tests/fts-sync.test.ts` 3. Make rebuild a first-class command Anything recoverable should be recoverable from a button. Files: `src/index/rebuild.ts` ### Done when - [ ] Deleting the database reproduces everything on restart - [ ] External edits reach the index - [ ] Rebuild completes and records its duration - [ ] No search result has missing content ### 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. ## M3 · Finding the note you half remember Search behaviour for short and partial words is a decision you made, not a surprise you discovered. ### Steps 1. Choose the tokeniser per column and write down why Both options are defensible; the undocumented one is not. Files: `src/index/schema.sql`, `docs/search.md` 2. Test the two-character query The shortest query is the one that surprises people. Files: `tests/short-query.test.ts` 3. Measure on a realistic folder Five thousand notes, generated, before you trust the timing. Files: `scripts/generate-notes.ts`, `tests/search-perf.test.ts` ### Done when - [ ] The tokeniser choice and reason are recorded - [ ] A two-character query behaves as documented - [ ] Substring search works where it was chosen - [ ] Search is within the stated time at five thousand notes ### 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. ## M4 · Links that survive a rename Renaming a note leaves every link to it working and every backlink intact. ### Steps 1. Link by id, render the title The id is the link; the title is the label. Files: `src/links/parse.ts`, `tests/rename.test.ts` 2. Keep unresolved links visible A broken link that silently disappears is a lost thought. Files: `src/links/unresolved.ts` 3. Build backlinks from the same table One source, so the two views cannot disagree. Files: `src/links/backlinks.ts` ### Done when - [ ] Renaming keeps inbound links and backlinks - [ ] Unresolved links are listed, not dropped - [ ] Backlinks and the links table agree - [ ] Links survive a rebuild ### 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. ## M5 · Already portable, and provably so (production only) Everything is readable without this application, and a folder copied elsewhere still works. ### Steps 1. Import without rewriting anyone's prose Add front matter; change nothing else. Files: `src/import/folder.ts`, `tests/import-idempotent.test.ts` 2. Snapshot the folder and test the restore A backup nobody has restored is a hope. Files: `src/backup/snapshot.ts`, `tests/restore.test.ts` 3. Detect sync conflict files Show both. Never merge, never quietly adopt one. Files: `src/index/conflicts.ts` ### Done when - [ ] Import assigns ids without altering prose - [ ] The export is readable elsewhere with a hashed manifest - [ ] Restore is exercised in the suite - [ ] Sync conflict files surface as conflicts ### Watch out - Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. ===== OPERATIONS.md ===== # Operations · Napkin ## Backup The notes folder, and only the notes folder. The index is derived and the snapshots are a convenience; if the folder survives, everything survives. Keep at least one copy somewhere the application cannot write to. ## Restore Copy the folder back and start the application with no index. A full scan should reproduce every note, tag, link and backlink, and that scan is the proof that the backup was complete. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: capture time from the shortcut, rebuild duration on the generated corpus, no search result with missing content, and a rename leaving backlinks intact. ## Incident checklist An empty backlinks panel usually means a link was written by filename somewhere. A search returning blank entries means the index desynchronised, and rebuild is the answer. A note that has gone missing is a file question, so look in the folder first and the database second. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Capture is confirmed focused within the stated time - [ ] A kill mid-sentence is confirmed to recover the text - [ ] Deleting the index is confirmed to reproduce everything - [ ] A two-character query is confirmed to behave as documented - [ ] A rename is confirmed to leave backlinks intact ## Launch constraint Do not market omitted Napkin capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The notes themselves. This is the thing to back up. NOTES_DIR=~/Documents/notes # Required. The index. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/napkin/index.db # Required. The global shortcut. Capture speed is the product. CAPTURE_SHORTCUT=CommandOrControl+Shift+Space # Required. How long after a keystroke the file is written atomically. WRITE_DEBOUNCE_MS=400 # Required. Folder snapshots. The restore is exercised in the suite. SNAPSHOT_DIR=~/.local/share/napkin/snapshots # Required. How many snapshots to retain before the oldest is deleted. SNAPSHOT_KEEP=14
# Napkin · indie build 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 sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | capture has to work with no network, no sign-in and no waiting | | Source of truth | One Markdown file per note in one folder, with front matter carrying the id | the files are the notes; anything else is a convenience | | Index | SQLite with FTS5 over an external content table, rebuildable by a scan | an index you cannot delete is a second source of truth | | Links | Stable ids in front matter, titles resolved at render time | a renamed note must not break every link pointing at it | | Capture | A global shortcut opening one text box that writes on a debounce and on blur | if capture takes two seconds you stop using it and the product is over | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and the Rust toolchain** · free - 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. - Verify: node --version prints v22 or higher and cargo --version succeeds - [ ] **SQLite with FTS5 available** · free - 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. - Verify: Creating a virtual table using fts5 succeeds, and so does creating one with the trigram tokeniser - [ ] **A notes folder, and a copy of it** · free - 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 - [ ] **A few thousand generated notes** · free - 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 - [ ] **Your tokeniser, debounce and snapshot retention** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients If one of those is essential to you, that is the reason to keep paying for Napkin, and the README should say so rather than pretend.
# Build brief · Napkin The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a capture-first notes app whose notes stay as plain Markdown files on your disk to replace Napkin. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript · capture has to work with no network, no sign-in and no waiting - Source of truth: One Markdown file per note in one folder, with front matter carrying the id · the files are the notes; anything else is a convenience - Index: SQLite with FTS5 over an external content table, rebuildable by a scan · an index you cannot delete is a second source of truth - Links: Stable ids in front matter, titles resolved at render time · a renamed note must not break every link pointing at it - Capture: A global shortcut opening one text box that writes on a debounce and on blur · if capture takes two seconds you stop using it and the product is over ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that cannot lose the thought Build: Register a global shortcut that opens one small window focused on an empty box. Append each change to a journal as you type. Write the note file atomically on a short debounce, on blur and on quit. On start, replay any journal entries newer than the file they belong to. Trap: 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. Done when: The capture window is focused and accepting input within a stated time from the shortcut, measured rather than felt. Killing the process mid-sentence and restarting recovers the text from the journal, tested by killing it. No note file is ever left partially written, tested by killing during a write. The journal is truncated once its entries are older than the file they wrote. Do not build yet: No search, links, tags or export. ### Phase 2 · The database is an index you can delete Build: Scan the folder into the index on start, then watch for changes. Keep the full-text index as an external content table over the notes rows, with triggers on insert, update and delete. Offer a rebuild command that drops and regenerates the whole index from the files. Trap: 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. Done when: Deleting the database file and restarting reproduces every note, tag and backlink identically, tested by comparing a full dump before and after. Editing a note file in another editor updates the index within a stated interval. The rebuild command completes and the timing is recorded, so a slow rebuild is visible before it becomes a problem. A search never returns a row whose content is missing, asserted by a test that deliberately desynchronises the index and then rebuilds. Do not build yet: No search interface, links or export yet. ### Phase 3 · Finding the note you half remember Build: Choose a tokeniser per indexed column and write down why. The default tokeniser gives you prefix matching and a small index; the trigram tokeniser gives substring matching and indexed pattern queries but matches nothing at all for a search under three characters, and costs much more space. Test the two-character query so its behaviour is stated rather than found. Trap: 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. Done when: The tokeniser choice per column is recorded in the schema file with its reason. A two-character query behaves as documented, asserted by a test rather than left to chance. A search for a substring inside a longer word returns the expected result for the columns where that was chosen. Search results return within a stated time on a folder of at least five thousand notes, measured with generated content. Do not build yet: No links, backlinks or export yet. ### Phase 4 · Links that survive a rename Build: Write links against the target's stable id and render the current title at display time. Keep unresolved links as rows rather than discarding them, and show them in one place so a typo is findable. Build the backlinks view from the same table. Trap: 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. Done when: Renaming a note leaves every inbound link working and every backlink present, tested by renaming a linked note. An unresolved link is stored and appears in a list of broken links rather than vanishing. The backlinks view and the links table agree, asserted directly. Links survive a full index rebuild, because they are parsed from the files. Do not build yet: No export or backup yet. ### Phase 5 · Already portable, and provably so Build: Import a folder of existing Markdown, assigning ids to files that lack them without rewriting their content otherwise. Export is a copy of the folder plus a manifest. Take automatic snapshots of the folder and exercise a restore in the suite. Handle a folder that a file sync tool has left conflicted copies in. Trap: Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. Done when: Importing a folder of plain Markdown assigns ids and leaves the body text byte identical apart from the front matter, tested. The export opens in any editor and the manifest lists every file with a hash. A restore from a snapshot is exercised in the suite rather than described. A conflicted copy left by a sync tool is surfaced as a conflict with both versions shown, never merged or silently imported as a new note. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a file copying one, and doing it badly loses notes. The folder is portable; leave the syncing to something that already does it. - Collaborative editing and sharing, which need an identity model this deliberately does not have. - A native mobile client, which is a second application rather than a feature of this one. - An importer for every other notes product. Write the one for the tool you are actually leaving, and make it exact. ### README must contain - That the Markdown files in the folder are the notes, and the database is an index that can be deleted and rebuilt - That capture appends to a journal and writes the file atomically, so a crash mid-sentence loses nothing - Which tokeniser each indexed column uses and what a query shorter than three characters does - That links are written against stable ids so a rename breaks nothing, and unresolved links are listed rather than dropped - That a file sync tool's conflicted copies are surfaced as conflicts and never merged
# Agent instructions · Napkin indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, One Markdown file per note in one folder, with front matter carrying the id, SQLite with FTS5 over an external content table, rebuildable by a scan, Stable ids in front matter, titles resolved at render time, A global shortcut opening one text box that writes on a debounce and on blur. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Napkin 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 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. ### Steps 1. Open one box on a global shortcut, focused The whole product lives or dies on this taking under a second. Files: `src-tauri/src/shortcut.rs`, `src/capture/Capture.tsx` 2. Append to a journal as you type The journal is the thing that survives the crash. Files: `src/store/journal.ts`, `tests/journal.test.ts` 3. Write the file atomically and replay on start Temporary file, flush, rename. Never write in place. Files: `src/store/atomic.ts`, `tests/crash-replay.test.ts` ### Done when - [ ] Capture is focused within the stated time - [ ] A kill mid-sentence recovers the text - [ ] No partially written note file is ever left - [ ] The journal truncates once its entries are durable ### 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. ## Phase 2 · The database is an index you can delete Deleting the database and restarting reproduces every note, tag and backlink from the folder. ### Steps 1. Scan on start, watch afterwards The folder is the truth and it can be edited elsewhere. Files: `src/index/scan.ts`, `src/index/watch.ts` 2. Add the insert, update and delete triggers External content without triggers is an index that lies. Files: `src/index/schema.sql`, `tests/fts-sync.test.ts` 3. Make rebuild a first-class command Anything recoverable should be recoverable from a button. Files: `src/index/rebuild.ts` ### Done when - [ ] Deleting the database reproduces everything on restart - [ ] External edits reach the index - [ ] Rebuild completes and records its duration - [ ] No search result has missing content ### 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. ## Phase 3 · Finding the note you half remember Search behaviour for short and partial words is a decision you made, not a surprise you discovered. ### Steps 1. Choose the tokeniser per column and write down why Both options are defensible; the undocumented one is not. Files: `src/index/schema.sql`, `docs/search.md` 2. Test the two-character query The shortest query is the one that surprises people. Files: `tests/short-query.test.ts` 3. Measure on a realistic folder Five thousand notes, generated, before you trust the timing. Files: `scripts/generate-notes.ts`, `tests/search-perf.test.ts` ### Done when - [ ] The tokeniser choice and reason are recorded - [ ] A two-character query behaves as documented - [ ] Substring search works where it was chosen - [ ] Search is within the stated time at five thousand notes ### 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. ## Phase 4 · Links that survive a rename Renaming a note leaves every link to it working and every backlink intact. ### Steps 1. Link by id, render the title The id is the link; the title is the label. Files: `src/links/parse.ts`, `tests/rename.test.ts` 2. Keep unresolved links visible A broken link that silently disappears is a lost thought. Files: `src/links/unresolved.ts` 3. Build backlinks from the same table One source, so the two views cannot disagree. Files: `src/links/backlinks.ts` ### Done when - [ ] Renaming keeps inbound links and backlinks - [ ] Unresolved links are listed, not dropped - [ ] Backlinks and the links table agree - [ ] Links survive a rebuild ### 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. ## Not in this build - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. ## After v1, if you want it - A daily note that is just a note with a predictable name, once capture has been reliable for a month - Saved searches stored as files in the folder, so they travel with the notes - A graph view built from the links table, which already holds everything it needs
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The notes themselves. This is the thing to back up. NOTES_DIR=~/Documents/notes # Required. The index. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/napkin/index.db # Required. The global shortcut. Capture speed is the product. CAPTURE_SHORTCUT=CommandOrControl+Shift+Space # Required. How long after a keystroke the file is written atomically. WRITE_DEBOUNCE_MS=400 # Required. Folder snapshots. The restore is exercised in the suite. SNAPSHOT_DIR=~/.local/share/napkin/snapshots # Required. How many snapshots to retain before the oldest is deleted. SNAPSHOT_KEEP=14
# Napkin · product brief ## Problem Napkin's solo core is compact: build a local-first idea collection app with fast capture, full-text search, backlinks, tags, and Markdown export. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on polish, sync, importers. ## Product outcome A notes application where capture is instant, the files are plain Markdown you could open in any editor, the database can be deleted without losing anything, and a rename never breaks a link. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Collaboration and sharing. - A mobile client. - Importers for every other notes product. - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing - native mobile clients ## Success criteria - Capture is confirmed focused within the stated time - A kill mid-sentence is confirmed to recover the text - Deleting the index is confirmed to reproduce everything - A two-character query is confirmed to behave as documented - A rename is confirmed to leave backlinks intact
# Build brief · Napkin The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a capture-first notes app whose notes stay as plain Markdown files on your disk to replace Napkin. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript · capture has to work with no network, no sign-in and no waiting - Source of truth: One Markdown file per note in one folder, with front matter carrying the id · the files are the notes; anything else is a convenience - Index: SQLite with FTS5 over an external content table, rebuildable by a scan · an index you cannot delete is a second source of truth - Links: Stable ids in front matter, titles resolved at render time · a renamed note must not break every link pointing at it - Capture: A global shortcut opening one text box that writes on a debounce and on blur · if capture takes two seconds you stop using it and the product is over ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that cannot lose the thought Build: Register a global shortcut that opens one small window focused on an empty box. Append each change to a journal as you type. Write the note file atomically on a short debounce, on blur and on quit. On start, replay any journal entries newer than the file they belong to. Trap: 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. Done when: The capture window is focused and accepting input within a stated time from the shortcut, measured rather than felt. Killing the process mid-sentence and restarting recovers the text from the journal, tested by killing it. No note file is ever left partially written, tested by killing during a write. The journal is truncated once its entries are older than the file they wrote. Do not build yet: No search, links, tags or export. ### Phase 2 · The database is an index you can delete Build: Scan the folder into the index on start, then watch for changes. Keep the full-text index as an external content table over the notes rows, with triggers on insert, update and delete. Offer a rebuild command that drops and regenerates the whole index from the files. Trap: 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. Done when: Deleting the database file and restarting reproduces every note, tag and backlink identically, tested by comparing a full dump before and after. Editing a note file in another editor updates the index within a stated interval. The rebuild command completes and the timing is recorded, so a slow rebuild is visible before it becomes a problem. A search never returns a row whose content is missing, asserted by a test that deliberately desynchronises the index and then rebuilds. Do not build yet: No search interface, links or export yet. ### Phase 3 · Finding the note you half remember Build: Choose a tokeniser per indexed column and write down why. The default tokeniser gives you prefix matching and a small index; the trigram tokeniser gives substring matching and indexed pattern queries but matches nothing at all for a search under three characters, and costs much more space. Test the two-character query so its behaviour is stated rather than found. Trap: 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. Done when: The tokeniser choice per column is recorded in the schema file with its reason. A two-character query behaves as documented, asserted by a test rather than left to chance. A search for a substring inside a longer word returns the expected result for the columns where that was chosen. Search results return within a stated time on a folder of at least five thousand notes, measured with generated content. Do not build yet: No links, backlinks or export yet. ### Phase 4 · Links that survive a rename Build: Write links against the target's stable id and render the current title at display time. Keep unresolved links as rows rather than discarding them, and show them in one place so a typo is findable. Build the backlinks view from the same table. Trap: 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. Done when: Renaming a note leaves every inbound link working and every backlink present, tested by renaming a linked note. An unresolved link is stored and appears in a list of broken links rather than vanishing. The backlinks view and the links table agree, asserted directly. Links survive a full index rebuild, because they are parsed from the files. Do not build yet: No export or backup yet. ### Phase 5 · Already portable, and provably so Build: Import a folder of existing Markdown, assigning ids to files that lack them without rewriting their content otherwise. Export is a copy of the folder plus a manifest. Take automatic snapshots of the folder and exercise a restore in the suite. Handle a folder that a file sync tool has left conflicted copies in. Trap: Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically. Done when: Importing a folder of plain Markdown assigns ids and leaves the body text byte identical apart from the front matter, tested. The export opens in any editor and the manifest lists every file with a hash. A restore from a snapshot is exercised in the suite rather than described. A conflicted copy left by a sync tool is surfaced as a conflict with both versions shown, never merged or silently imported as a new note. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-device sync, which is a conflict resolution problem rather than a file copying one, and doing it badly loses notes. The folder is portable; leave the syncing to something that already does it. - Collaborative editing and sharing, which need an identity model this deliberately does not have. - A native mobile client, which is a second application rather than a feature of this one. - An importer for every other notes product. Write the one for the tool you are actually leaving, and make it exact. ### README must contain - That the Markdown files in the folder are the notes, and the database is an index that can be deleted and rebuilt - That capture appends to a journal and writes the file atomically, so a crash mid-sentence loses nothing - Which tokeniser each indexed column uses and what a query shorter than three characters does - That links are written against stable ids so a rename breaks nothing, and unresolved links are listed rather than dropped - That a file sync tool's conflicted copies are surfaced as conflicts and never merged
# Architecture · Napkin ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | capture has to work with no network, no sign-in and no waiting | | Source of truth | One Markdown file per note in one folder, with front matter carrying the id | the files are the notes; anything else is a convenience | | Index | SQLite with FTS5 over an external content table, rebuildable by a scan | an index you cannot delete is a second source of truth | | Links | Stable ids in front matter, titles resolved at render time | a renamed note must not break every link pointing at it | | Capture | A global shortcut opening one text box that writes on a debounce and on blur | if capture takes two seconds you stop using it and the product is over | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | The shortcut, the journal and atomic writes. | Any front end, provided the journal and the atomic write stay. | | Index | Scan, watch, triggers and rebuild. | Any index, provided it is rebuildable from the folder alone. | | Search | Tokeniser choice and query behaviour. | Either tokeniser, provided the choice and the short-query behaviour are documented. | | Links | Id-based links, backlinks and unresolved links. | Not swappable for filename links, which break on rename. | | Portability | Import, export, snapshots and conflict detection. | Any snapshot tool whose restore is exercised in the suite. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `NOTES_DIR` · required · The notes themselves. This is the thing to back up. - `INDEX_PATH` · required · The index. Safe to delete; it rebuilds from the folder. - `CAPTURE_SHORTCUT` · required · The global shortcut. Capture speed is the product. - `WRITE_DEBOUNCE_MS` · required · How long after a keystroke the file is written atomically. - `SNAPSHOT_DIR` · required · Folder snapshots. The restore is exercised in the suite. - `SNAPSHOT_KEEP` · required · How many snapshots to retain before the oldest is deleted. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Napkin product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, One Markdown file per note in one folder, with front matter carrying the id, SQLite with FTS5 over an external content table, rebuildable by a scan, Stable ids in front matter, titles resolved at render time, A global shortcut opening one text box that writes on a debounce and on blur. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically.
# Delivery milestones · Napkin Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · 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. ### Steps 1. Open one box on a global shortcut, focused The whole product lives or dies on this taking under a second. Files: `src-tauri/src/shortcut.rs`, `src/capture/Capture.tsx` 2. Append to a journal as you type The journal is the thing that survives the crash. Files: `src/store/journal.ts`, `tests/journal.test.ts` 3. Write the file atomically and replay on start Temporary file, flush, rename. Never write in place. Files: `src/store/atomic.ts`, `tests/crash-replay.test.ts` ### Done when - [ ] Capture is focused within the stated time - [ ] A kill mid-sentence recovers the text - [ ] No partially written note file is ever left - [ ] The journal truncates once its entries are durable ### 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. ## M2 · The database is an index you can delete Deleting the database and restarting reproduces every note, tag and backlink from the folder. ### Steps 1. Scan on start, watch afterwards The folder is the truth and it can be edited elsewhere. Files: `src/index/scan.ts`, `src/index/watch.ts` 2. Add the insert, update and delete triggers External content without triggers is an index that lies. Files: `src/index/schema.sql`, `tests/fts-sync.test.ts` 3. Make rebuild a first-class command Anything recoverable should be recoverable from a button. Files: `src/index/rebuild.ts` ### Done when - [ ] Deleting the database reproduces everything on restart - [ ] External edits reach the index - [ ] Rebuild completes and records its duration - [ ] No search result has missing content ### 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. ## M3 · Finding the note you half remember Search behaviour for short and partial words is a decision you made, not a surprise you discovered. ### Steps 1. Choose the tokeniser per column and write down why Both options are defensible; the undocumented one is not. Files: `src/index/schema.sql`, `docs/search.md` 2. Test the two-character query The shortest query is the one that surprises people. Files: `tests/short-query.test.ts` 3. Measure on a realistic folder Five thousand notes, generated, before you trust the timing. Files: `scripts/generate-notes.ts`, `tests/search-perf.test.ts` ### Done when - [ ] The tokeniser choice and reason are recorded - [ ] A two-character query behaves as documented - [ ] Substring search works where it was chosen - [ ] Search is within the stated time at five thousand notes ### 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. ## M4 · Links that survive a rename Renaming a note leaves every link to it working and every backlink intact. ### Steps 1. Link by id, render the title The id is the link; the title is the label. Files: `src/links/parse.ts`, `tests/rename.test.ts` 2. Keep unresolved links visible A broken link that silently disappears is a lost thought. Files: `src/links/unresolved.ts` 3. Build backlinks from the same table One source, so the two views cannot disagree. Files: `src/links/backlinks.ts` ### Done when - [ ] Renaming keeps inbound links and backlinks - [ ] Unresolved links are listed, not dropped - [ ] Backlinks and the links table agree - [ ] Links survive a rebuild ### 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. ## M5 · Already portable, and provably so (production only) Everything is readable without this application, and a folder copied elsewhere still works. ### Steps 1. Import without rewriting anyone's prose Add front matter; change nothing else. Files: `src/import/folder.ts`, `tests/import-idempotent.test.ts` 2. Snapshot the folder and test the restore A backup nobody has restored is a hope. Files: `src/backup/snapshot.ts`, `tests/restore.test.ts` 3. Detect sync conflict files Show both. Never merge, never quietly adopt one. Files: `src/index/conflicts.ts` ### Done when - [ ] Import assigns ids without altering prose - [ ] The export is readable elsewhere with a hashed manifest - [ ] Restore is exercised in the suite - [ ] Sync conflict files surface as conflicts ### Watch out - Keeping the notes in a folder that a file sync tool watches feels like free sync until two devices edit the same note offline and the tool drops a second file beside it with 'conflicted copy' in the name, which your scanner then imports as a separate note. Detect the pattern, surface both versions side by side, and never merge them automatically.
# Operations · Napkin ## Backup The notes folder, and only the notes folder. The index is derived and the snapshots are a convenience; if the folder survives, everything survives. Keep at least one copy somewhere the application cannot write to. ## Restore Copy the folder back and start the application with no index. A full scan should reproduce every note, tag, link and backlink, and that scan is the proof that the backup was complete. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the checks live in the suite: capture time from the shortcut, rebuild duration on the generated corpus, no search result with missing content, and a rename leaving backlinks intact. ## Incident checklist An empty backlinks panel usually means a link was written by filename somewhere. A search returning blank entries means the index desynchronised, and rebuild is the answer. A note that has gone missing is a file question, so look in the folder first and the database second. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Capture is confirmed focused within the stated time - [ ] A kill mid-sentence is confirmed to recover the text - [ ] Deleting the index is confirmed to reproduce everything - [ ] A two-character query is confirmed to behave as documented - [ ] A rename is confirmed to leave backlinks intact ## Launch constraint Do not market omitted Napkin capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The notes themselves. This is the thing to back up. NOTES_DIR=~/Documents/notes # Required. The index. Safe to delete; it rebuilds from the folder. INDEX_PATH=~/.local/share/napkin/index.db # Required. The global shortcut. Capture speed is the product. CAPTURE_SHORTCUT=CommandOrControl+Shift+Space # Required. How long after a keystroke the file is written atomically. WRITE_DEBOUNCE_MS=400 # Required. Folder snapshots. The restore is exercised in the suite. SNAPSHOT_DIR=~/.local/share/napkin/snapshots # Required. How many snapshots to retain before the oldest is deleted. SNAPSHOT_KEEP=14
$ choose a build depth, inspect the files, then open the complete pack in your agent
Napkin: People pay for a writing surface they trust for years, plus migration tools, sync, and tiny interaction details that disappear into habit.
xconflict-safe multi-device sync
xmature importers and exporters
xcollaborative editing and sharing
xnative mobile clients
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Napkin pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free download / trial | $0/user | $0/user | Subscription is required for continuing use; official listing does not publish a trial length or numeric idea cap |
| all features, monthly | $5/user | — | All app features; offline use with tens of thousands of ideas; iPhone only |
| all features, yearly | — | $3.33/user | All app features; offline use with tens of thousands of ideas; iPhone only |
free tierno permanent free tier; trial length and numeric allowance are not published
billingmonthly ($5) + annual ($40/year) through Apple
hidden costsThe App Store also lists older generic $3/month and $30/year SKUs, but their current availability is unclear. Subscription is required, and the rebuilt app currently requires iOS 26 on iPhone.
verified 2026-08-13 · source ↗
Vibecode Napkin
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Napkin replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Napkin cost?
Napkin's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Napkin?
Honestly: conflict-safe multi-device sync; mature importers and exporters; collaborative editing and sharing; native mobile clients. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Napkin?
Yes: Atomic (Short notes turn into a semantic graph without waiting for you to become an archivist.) The prompt is for when you want it exactly your way.