Vibecode Supernotes
track this build4 phases, 12 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Supernotes, capture small linked cards, tag them, and review or share focused collections. The hard boundary is real-time collaboration, mobile polish, sync, and card-based workflow refinement, plus sync, collaboration, and capture polish.
You are building a lean indie version of Supernotes. 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 ===== # Supernotes · indie build A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Estimated effort: **weekend**. 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 | a desktop application that reads a folder you chose and nothing else | | Storage | One Markdown file per card with front matter, in a folder you pick | the notes have to open in any editor in ten years | | Index | SQLite, derived only, rebuilt from the folder by one command | an index that cannot be thrown away has become a second source of truth | | Identity | A stable id in front matter, independent of the filename and the title | titles change and filenames follow them, links must not | | Writes | Temporary file, flush, atomic replace, with the watcher ignoring its own writes | truncated notes and infinite watch loops are the two classic failures here | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and finding out during the first build is an avoidable hour. - Get it: nodejs.org and rustup.rs, plus the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **A folder of real notes to test against** · free - Why: Filename sanitising, link resolution and rebuild all behave differently on two hundred real cards than on five invented ones. - Get it: Export an existing notebook to Markdown, or copy a folder of notes you already keep. - Verify: The folder indexes without errors and the card count matches the file count - [ ] **Access to a second operating system** · free - Why: The filename rules only prove themselves on a filesystem with different rules from the one you develop on, and a virtual machine is enough. - Get it: A Windows or macOS virtual machine, or a colleague willing to copy a folder and open it. - Verify: The fixture vault copies across and every card opens - [ ] **A second Markdown editor** · free - Why: The claim that the folder is portable is only true if another tool renders it, and that is a five minute check nobody does unless it is a prerequisite. - Get it: Any Markdown editor that opens a folder. - Verify: Cards, front matter and relative images all render in it ## 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: - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. - real-time collaboration, mobile polish, sync, and card-based workflow refinement - frictionless mobile capture - real-time team editing - hosted publishing - proprietary AI memory If one of those is essential to you, that is the reason to keep paying for Supernotes, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Supernotes 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 local card notebook whose Markdown folder stays readable and usable without it to replace Supernotes. 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 · a desktop application that reads a folder you chose and nothing else - Storage: One Markdown file per card with front matter, in a folder you pick · the notes have to open in any editor in ten years - Index: SQLite, derived only, rebuilt from the folder by one command · an index that cannot be thrown away has become a second source of truth - Identity: A stable id in front matter, independent of the filename and the title · titles change and filenames follow them, links must not - Writes: Temporary file, flush, atomic replace, with the watcher ignoring its own writes · truncated notes and infinite watch loops are the two classic failures here ### Data model (create this before Phase 1) - card file front matter(id, title, tags, created, updated, pinned) · the file is the record and everything else is derived from it - cards(id, path, title, tags_json, created_at, updated_at, content_hash, missing_since) · an index row, rebuildable at any time - links(from_id, to_id, display_text, ordinal, resolved) · unresolved links are kept and shown rather than dropped - attachments(card_id, relative_path, sha256, bytes) · stored beside the cards and referenced by relative path only - conflicts(id, card_id, path, detected_at, resolved_at, kept) · an external edit that clashes becomes a second file, never an overwrite - index_state(rebuilt_at, cards_indexed, schema_version) · so you can see when the derived data was last derived - Every table except the front matter can be deleted and rebuilt, which is the test of whether the files really are the source of truth. ### Phase 1 · Files are the truth, the index is disposable Build: Store each card as a Markdown file with front matter carrying a stable id. Index titles, tags, links and timestamps into SQLite. Provide a rebuild command that discards the index and reconstructs it from the folder, and run it in the test suite. Trap: Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. Done when: Deleting the index and running rebuild restores every card, tag, link and title, exercised in the suite. Creating a card writes a Markdown file that opens correctly in a plain text editor, front matter included. The id in front matter is generated once and never derived from the title. Search returns results from the rebuilt index with no reference to the old one. Do not build yet: No filename rules, backlinks, watching or attachments. ### Phase 2 · Names that survive every filesystem Build: Derive filenames from titles through a sanitiser: strip the reserved characters, refuse the reserved device names, trim trailing spaces and periods, normalise Unicode to one form, cap the length with room for the folder path, and resolve collisions case insensitively. Trap: Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. Done when: A card titled with each reserved device name, each reserved character, and a trailing period produces a valid filename, tested case by case. Two cards whose titles differ only in case produce two distinct files that do not collide on a case insensitive filesystem. Titles containing accented characters are normalised to one form on write so the same card is not seen as two after a sync. Generated paths stay within the configured maximum length including the folder prefix. Do not build yet: No backlinks, watching, attachments or export yet. ### Phase 3 · Links that survive a rename Build: Write links by id with display text alongside. Resolve them at render time from the index. Renaming a card updates the display text in linking cards but not the target. Show unresolved links as visibly broken with the id they point at, and list backlinks per card. Trap: Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. Done when: Renaming a card leaves every link to it working, tested across three linking cards. A link to a deleted card renders as broken with the id shown rather than as plain text. Backlinks for a card are derived from the index and match a hand count on a fixture folder. Link display text updates on rename without rewriting the target id. Do not build yet: No watching, conflicts, attachments or export yet. ### Phase 4 · Edits from outside, without losing one Build: Watch the folder, comparing modification time and content hash, and ignore the writes the application itself just made. When the file changed underneath an open editor, keep both: write a conflict copy beside the card and show a comparison. Never save over unseen content. Trap: A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. Done when: Editing a card in another editor while it is open produces a conflict copy and a comparison, with both versions intact, tested. The application's own writes never trigger a re-index of unchanged content, verified by counting index writes during a save. A file deleted outside is marked missing rather than removed from the index. A crash during a save leaves either the old file or the new one, never a truncated one. Do not build yet: No attachments, export or backup drill yet. ### Phase 5 · The folder already is the export Build: Store attachments beside the cards and reference them by relative path. Keep front matter to a documented, small set of keys. Add a backup command that copies the folder and a restore drill that opens the copy as a fresh vault. Confirm the folder renders in another Markdown editor. Trap: Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. Done when: A copy of the vault opened from a different path renders every attachment, tested by copying the fixture vault elsewhere. Front matter keys are documented and any unknown key is preserved untouched on save rather than stripped. The backup command produces a folder that opens as a working vault, exercised in the suite. Opening the folder in a different Markdown editor shows readable cards with working relative image links. Do not build yet: Nothing after this. ### Out of scope (and why) - A sync server, which is the real subscription and is a conflict resolution problem that deserves more than a weekend. - Real time collaborative editing, which needs a merge model and a server and changes the file on disk story completely. - Mobile applications, where capture friction is the entire point and a desktop shortcut does not substitute. - Hosted public publishing of cards, which brings hosting, access control and takedown into a local notebook. ### README must contain - That the Markdown folder is the source of truth and the index can be deleted and rebuilt with one command - The exact filename rules applied, including reserved device names, reserved characters and the path length cap - That links are stored by id with display text, so renaming never breaks one - How external edits are detected and that a clash produces a conflict copy rather than an overwrite - That attachments live inside the vault and are referenced relatively, so a copied folder still works ===== AGENTS.md ===== # Agent instructions · Supernotes 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 card with front matter, in a folder you pick, SQLite, derived only, rebuilt from the folder by one command, A stable id in front matter, independent of the filename and the title, Temporary file, flush, atomic replace, with the watcher ignoring its own writes. 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 - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ===== BUILD_PLAN.md ===== # Build plan · Supernotes A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files are the truth, the index is disposable Deleting the database and rebuilding loses nothing. ### Steps 1. Write the file before touching the index The order is the guarantee: a crash between the two costs an index entry, not a note. Files: `src/cards/write.ts` 2. Generate ids independent of the title Because the title is the thing most likely to change. Files: `src/cards/id.ts` 3. Make rebuild a tested command If rebuild is not in the suite, the index quietly becomes load bearing. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] Rebuild from the folder restores everything - [ ] Cards open correctly in a plain text editor - [ ] Ids are not derived from titles - [ ] Search works from a freshly rebuilt index ### Watch out - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. ## Phase 2 · Names that survive every filesystem A card title can contain anything and the filename still works everywhere. ### Steps 1. Sanitise against the documented rules Reserved characters, reserved device names, trailing space and period, path length. Files: `src/files/sanitise.ts`, `tests/filenames.test.ts` 2. Normalise Unicode on write Otherwise one accented title becomes two cards after it crosses a filesystem. Files: `src/files/sanitise.ts` 3. Keep the real title in front matter The filename is a convenience; the title is the data. Files: `src/cards/write.ts` ### Done when - [ ] Reserved names and characters all produce valid filenames - [ ] Case only title differences do not collide - [ ] Accented titles are normalised on write - [ ] Paths stay within the configured length limit ### Watch out - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. ## Phase 3 · Links that survive a rename Renaming a card never breaks a link to it. ### Steps 1. Link by id with display text Two pieces of information, and only one of them is allowed to change. Files: `src/links/parse.ts` 2. Render unresolved links as broken Visibly, with the id, so it can be repaired rather than guessed at. Files: `src/links/render.tsx` 3. Derive backlinks from the index Derived means a rebuild fixes them, which is the point of the index. Files: `src/links/backlinks.ts`, `tests/backlinks.test.ts` ### Done when - [ ] Renaming keeps every link working - [ ] Links to deleted cards render as broken with the id - [ ] Backlinks match a hand count on a fixture - [ ] Rename updates display text only ### Watch out - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. ## Phase 4 · Edits from outside, without losing one A card changed in another editor is picked up and never overwritten. ### Steps 1. Ignore your own writes by content hash Not by a timer, which is how real edits get eaten. Files: `src/watch/index.ts` 2. Keep both sides as a conflict copy Two files and a comparison beats a merge that guessed. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Write atomically, always Temporary file, flush, replace, for every single save. Files: `src/files/atomic.ts` ### Done when - [ ] External edits produce a conflict copy with both versions - [ ] Own writes do not cause redundant re-indexing - [ ] Externally deleted files are marked missing - [ ] A crash mid save never truncates a card ### Watch out - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ## Not in this build - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. ## After v1, if you want it - Daily notes, which are a card template and a date in the filename and nothing more - Saved searches as cards, so a collection is a note rather than a feature - Sync through a folder service you already pay for, once conflict copies have proven themselves ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. This is all of the user's data. VAULT_PATH=~/Notes # Required. Derived only. Deleting it must cost nothing but time. INDEX_PATH=~/.local/share/cards/index.db # Required. Leaves headroom under the platform path limit including the vault prefix. MAX_FILENAME_LENGTH=120 # Required. Applied on write so one accented title cannot become two cards. UNICODE_NORMALISATION=NFC # Required. Appended to the copy kept when an external edit clashes. CONFLICT_SUFFIX=.conflict # Required. Coalesces editor save storms. It is not how own writes are ignored. WATCH_DEBOUNCE_MS=200
You are building a lean indie version of Supernotes. 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 ===== # Supernotes · indie build A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Estimated effort: **weekend**. 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 | a desktop application that reads a folder you chose and nothing else | | Storage | One Markdown file per card with front matter, in a folder you pick | the notes have to open in any editor in ten years | | Index | SQLite, derived only, rebuilt from the folder by one command | an index that cannot be thrown away has become a second source of truth | | Identity | A stable id in front matter, independent of the filename and the title | titles change and filenames follow them, links must not | | Writes | Temporary file, flush, atomic replace, with the watcher ignoring its own writes | truncated notes and infinite watch loops are the two classic failures here | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and finding out during the first build is an avoidable hour. - Get it: nodejs.org and rustup.rs, plus the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **A folder of real notes to test against** · free - Why: Filename sanitising, link resolution and rebuild all behave differently on two hundred real cards than on five invented ones. - Get it: Export an existing notebook to Markdown, or copy a folder of notes you already keep. - Verify: The folder indexes without errors and the card count matches the file count - [ ] **Access to a second operating system** · free - Why: The filename rules only prove themselves on a filesystem with different rules from the one you develop on, and a virtual machine is enough. - Get it: A Windows or macOS virtual machine, or a colleague willing to copy a folder and open it. - Verify: The fixture vault copies across and every card opens - [ ] **A second Markdown editor** · free - Why: The claim that the folder is portable is only true if another tool renders it, and that is a five minute check nobody does unless it is a prerequisite. - Get it: Any Markdown editor that opens a folder. - Verify: Cards, front matter and relative images all render in it ## 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: - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. - real-time collaboration, mobile polish, sync, and card-based workflow refinement - frictionless mobile capture - real-time team editing - hosted publishing - proprietary AI memory If one of those is essential to you, that is the reason to keep paying for Supernotes, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Supernotes 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 local card notebook whose Markdown folder stays readable and usable without it to replace Supernotes. 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 · a desktop application that reads a folder you chose and nothing else - Storage: One Markdown file per card with front matter, in a folder you pick · the notes have to open in any editor in ten years - Index: SQLite, derived only, rebuilt from the folder by one command · an index that cannot be thrown away has become a second source of truth - Identity: A stable id in front matter, independent of the filename and the title · titles change and filenames follow them, links must not - Writes: Temporary file, flush, atomic replace, with the watcher ignoring its own writes · truncated notes and infinite watch loops are the two classic failures here ### Data model (create this before Phase 1) - card file front matter(id, title, tags, created, updated, pinned) · the file is the record and everything else is derived from it - cards(id, path, title, tags_json, created_at, updated_at, content_hash, missing_since) · an index row, rebuildable at any time - links(from_id, to_id, display_text, ordinal, resolved) · unresolved links are kept and shown rather than dropped - attachments(card_id, relative_path, sha256, bytes) · stored beside the cards and referenced by relative path only - conflicts(id, card_id, path, detected_at, resolved_at, kept) · an external edit that clashes becomes a second file, never an overwrite - index_state(rebuilt_at, cards_indexed, schema_version) · so you can see when the derived data was last derived - Every table except the front matter can be deleted and rebuilt, which is the test of whether the files really are the source of truth. ### Phase 1 · Files are the truth, the index is disposable Build: Store each card as a Markdown file with front matter carrying a stable id. Index titles, tags, links and timestamps into SQLite. Provide a rebuild command that discards the index and reconstructs it from the folder, and run it in the test suite. Trap: Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. Done when: Deleting the index and running rebuild restores every card, tag, link and title, exercised in the suite. Creating a card writes a Markdown file that opens correctly in a plain text editor, front matter included. The id in front matter is generated once and never derived from the title. Search returns results from the rebuilt index with no reference to the old one. Do not build yet: No filename rules, backlinks, watching or attachments. ### Phase 2 · Names that survive every filesystem Build: Derive filenames from titles through a sanitiser: strip the reserved characters, refuse the reserved device names, trim trailing spaces and periods, normalise Unicode to one form, cap the length with room for the folder path, and resolve collisions case insensitively. Trap: Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. Done when: A card titled with each reserved device name, each reserved character, and a trailing period produces a valid filename, tested case by case. Two cards whose titles differ only in case produce two distinct files that do not collide on a case insensitive filesystem. Titles containing accented characters are normalised to one form on write so the same card is not seen as two after a sync. Generated paths stay within the configured maximum length including the folder prefix. Do not build yet: No backlinks, watching, attachments or export yet. ### Phase 3 · Links that survive a rename Build: Write links by id with display text alongside. Resolve them at render time from the index. Renaming a card updates the display text in linking cards but not the target. Show unresolved links as visibly broken with the id they point at, and list backlinks per card. Trap: Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. Done when: Renaming a card leaves every link to it working, tested across three linking cards. A link to a deleted card renders as broken with the id shown rather than as plain text. Backlinks for a card are derived from the index and match a hand count on a fixture folder. Link display text updates on rename without rewriting the target id. Do not build yet: No watching, conflicts, attachments or export yet. ### Phase 4 · Edits from outside, without losing one Build: Watch the folder, comparing modification time and content hash, and ignore the writes the application itself just made. When the file changed underneath an open editor, keep both: write a conflict copy beside the card and show a comparison. Never save over unseen content. Trap: A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. Done when: Editing a card in another editor while it is open produces a conflict copy and a comparison, with both versions intact, tested. The application's own writes never trigger a re-index of unchanged content, verified by counting index writes during a save. A file deleted outside is marked missing rather than removed from the index. A crash during a save leaves either the old file or the new one, never a truncated one. Do not build yet: No attachments, export or backup drill yet. ### Phase 5 · The folder already is the export Build: Store attachments beside the cards and reference them by relative path. Keep front matter to a documented, small set of keys. Add a backup command that copies the folder and a restore drill that opens the copy as a fresh vault. Confirm the folder renders in another Markdown editor. Trap: Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. Done when: A copy of the vault opened from a different path renders every attachment, tested by copying the fixture vault elsewhere. Front matter keys are documented and any unknown key is preserved untouched on save rather than stripped. The backup command produces a folder that opens as a working vault, exercised in the suite. Opening the folder in a different Markdown editor shows readable cards with working relative image links. Do not build yet: Nothing after this. ### Out of scope (and why) - A sync server, which is the real subscription and is a conflict resolution problem that deserves more than a weekend. - Real time collaborative editing, which needs a merge model and a server and changes the file on disk story completely. - Mobile applications, where capture friction is the entire point and a desktop shortcut does not substitute. - Hosted public publishing of cards, which brings hosting, access control and takedown into a local notebook. ### README must contain - That the Markdown folder is the source of truth and the index can be deleted and rebuilt with one command - The exact filename rules applied, including reserved device names, reserved characters and the path length cap - That links are stored by id with display text, so renaming never breaks one - How external edits are detected and that a clash produces a conflict copy rather than an overwrite - That attachments live inside the vault and are referenced relatively, so a copied folder still works ===== AGENTS.md ===== # Agent instructions · Supernotes 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 card with front matter, in a folder you pick, SQLite, derived only, rebuilt from the folder by one command, A stable id in front matter, independent of the filename and the title, Temporary file, flush, atomic replace, with the watcher ignoring its own writes. 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 - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ===== BUILD_PLAN.md ===== # Build plan · Supernotes A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files are the truth, the index is disposable Deleting the database and rebuilding loses nothing. ### Steps 1. Write the file before touching the index The order is the guarantee: a crash between the two costs an index entry, not a note. Files: `src/cards/write.ts` 2. Generate ids independent of the title Because the title is the thing most likely to change. Files: `src/cards/id.ts` 3. Make rebuild a tested command If rebuild is not in the suite, the index quietly becomes load bearing. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] Rebuild from the folder restores everything - [ ] Cards open correctly in a plain text editor - [ ] Ids are not derived from titles - [ ] Search works from a freshly rebuilt index ### Watch out - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. ## Phase 2 · Names that survive every filesystem A card title can contain anything and the filename still works everywhere. ### Steps 1. Sanitise against the documented rules Reserved characters, reserved device names, trailing space and period, path length. Files: `src/files/sanitise.ts`, `tests/filenames.test.ts` 2. Normalise Unicode on write Otherwise one accented title becomes two cards after it crosses a filesystem. Files: `src/files/sanitise.ts` 3. Keep the real title in front matter The filename is a convenience; the title is the data. Files: `src/cards/write.ts` ### Done when - [ ] Reserved names and characters all produce valid filenames - [ ] Case only title differences do not collide - [ ] Accented titles are normalised on write - [ ] Paths stay within the configured length limit ### Watch out - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. ## Phase 3 · Links that survive a rename Renaming a card never breaks a link to it. ### Steps 1. Link by id with display text Two pieces of information, and only one of them is allowed to change. Files: `src/links/parse.ts` 2. Render unresolved links as broken Visibly, with the id, so it can be repaired rather than guessed at. Files: `src/links/render.tsx` 3. Derive backlinks from the index Derived means a rebuild fixes them, which is the point of the index. Files: `src/links/backlinks.ts`, `tests/backlinks.test.ts` ### Done when - [ ] Renaming keeps every link working - [ ] Links to deleted cards render as broken with the id - [ ] Backlinks match a hand count on a fixture - [ ] Rename updates display text only ### Watch out - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. ## Phase 4 · Edits from outside, without losing one A card changed in another editor is picked up and never overwritten. ### Steps 1. Ignore your own writes by content hash Not by a timer, which is how real edits get eaten. Files: `src/watch/index.ts` 2. Keep both sides as a conflict copy Two files and a comparison beats a merge that guessed. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Write atomically, always Temporary file, flush, replace, for every single save. Files: `src/files/atomic.ts` ### Done when - [ ] External edits produce a conflict copy with both versions - [ ] Own writes do not cause redundant re-indexing - [ ] Externally deleted files are marked missing - [ ] A crash mid save never truncates a card ### Watch out - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ## Not in this build - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. ## After v1, if you want it - Daily notes, which are a card template and a date in the filename and nothing more - Saved searches as cards, so a collection is a note rather than a feature - Sync through a folder service you already pay for, once conflict copies have proven themselves ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. This is all of the user's data. VAULT_PATH=~/Notes # Required. Derived only. Deleting it must cost nothing but time. INDEX_PATH=~/.local/share/cards/index.db # Required. Leaves headroom under the platform path limit including the vault prefix. MAX_FILENAME_LENGTH=120 # Required. Applied on write so one accented title cannot become two cards. UNICODE_NORMALISATION=NFC # Required. Appended to the copy kept when an external edit clashes. CONFLICT_SUFFIX=.conflict # Required. Coalesces editor save storms. It is not how own writes are ignored. WATCH_DEBOUNCE_MS=200
You are building a production product version of Supernotes. 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 ===== # Supernotes · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Supernotes, capture small linked cards, tag them, and review or share focused collections. The hard boundary is real-time collaboration, mobile polish, sync, and card-based workflow refinement, plus sync, collaboration, and capture polish. ## Product outcome A notebook whose folder of Markdown files is the whole product, where the index can be deleted without loss, filenames work on every filesystem, links survive renames, and an edit made in another editor is never overwritten. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - local folder access - optional sync folder chosen by the user ## Explicit non-goals for v1 - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. - real-time collaboration, mobile polish, sync, and card-based workflow refinement - frictionless mobile capture - real-time team editing - hosted publishing - proprietary AI memory ## Success criteria - Rebuild from the folder is confirmed lossless - Every reserved name and character is confirmed handled - Renaming is confirmed to keep links working - An external edit is confirmed to produce a conflict copy - A copied vault is confirmed to render attachments ===== BRIEF.md ===== # Build brief · Supernotes 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 local card notebook whose Markdown folder stays readable and usable without it to replace Supernotes. 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 · a desktop application that reads a folder you chose and nothing else - Storage: One Markdown file per card with front matter, in a folder you pick · the notes have to open in any editor in ten years - Index: SQLite, derived only, rebuilt from the folder by one command · an index that cannot be thrown away has become a second source of truth - Identity: A stable id in front matter, independent of the filename and the title · titles change and filenames follow them, links must not - Writes: Temporary file, flush, atomic replace, with the watcher ignoring its own writes · truncated notes and infinite watch loops are the two classic failures here ### Data model (create this before Phase 1) - card file front matter(id, title, tags, created, updated, pinned) · the file is the record and everything else is derived from it - cards(id, path, title, tags_json, created_at, updated_at, content_hash, missing_since) · an index row, rebuildable at any time - links(from_id, to_id, display_text, ordinal, resolved) · unresolved links are kept and shown rather than dropped - attachments(card_id, relative_path, sha256, bytes) · stored beside the cards and referenced by relative path only - conflicts(id, card_id, path, detected_at, resolved_at, kept) · an external edit that clashes becomes a second file, never an overwrite - index_state(rebuilt_at, cards_indexed, schema_version) · so you can see when the derived data was last derived - Every table except the front matter can be deleted and rebuilt, which is the test of whether the files really are the source of truth. ### Phase 1 · Files are the truth, the index is disposable Build: Store each card as a Markdown file with front matter carrying a stable id. Index titles, tags, links and timestamps into SQLite. Provide a rebuild command that discards the index and reconstructs it from the folder, and run it in the test suite. Trap: Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. Done when: Deleting the index and running rebuild restores every card, tag, link and title, exercised in the suite. Creating a card writes a Markdown file that opens correctly in a plain text editor, front matter included. The id in front matter is generated once and never derived from the title. Search returns results from the rebuilt index with no reference to the old one. Do not build yet: No filename rules, backlinks, watching or attachments. ### Phase 2 · Names that survive every filesystem Build: Derive filenames from titles through a sanitiser: strip the reserved characters, refuse the reserved device names, trim trailing spaces and periods, normalise Unicode to one form, cap the length with room for the folder path, and resolve collisions case insensitively. Trap: Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. Done when: A card titled with each reserved device name, each reserved character, and a trailing period produces a valid filename, tested case by case. Two cards whose titles differ only in case produce two distinct files that do not collide on a case insensitive filesystem. Titles containing accented characters are normalised to one form on write so the same card is not seen as two after a sync. Generated paths stay within the configured maximum length including the folder prefix. Do not build yet: No backlinks, watching, attachments or export yet. ### Phase 3 · Links that survive a rename Build: Write links by id with display text alongside. Resolve them at render time from the index. Renaming a card updates the display text in linking cards but not the target. Show unresolved links as visibly broken with the id they point at, and list backlinks per card. Trap: Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. Done when: Renaming a card leaves every link to it working, tested across three linking cards. A link to a deleted card renders as broken with the id shown rather than as plain text. Backlinks for a card are derived from the index and match a hand count on a fixture folder. Link display text updates on rename without rewriting the target id. Do not build yet: No watching, conflicts, attachments or export yet. ### Phase 4 · Edits from outside, without losing one Build: Watch the folder, comparing modification time and content hash, and ignore the writes the application itself just made. When the file changed underneath an open editor, keep both: write a conflict copy beside the card and show a comparison. Never save over unseen content. Trap: A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. Done when: Editing a card in another editor while it is open produces a conflict copy and a comparison, with both versions intact, tested. The application's own writes never trigger a re-index of unchanged content, verified by counting index writes during a save. A file deleted outside is marked missing rather than removed from the index. A crash during a save leaves either the old file or the new one, never a truncated one. Do not build yet: No attachments, export or backup drill yet. ### Phase 5 · The folder already is the export Build: Store attachments beside the cards and reference them by relative path. Keep front matter to a documented, small set of keys. Add a backup command that copies the folder and a restore drill that opens the copy as a fresh vault. Confirm the folder renders in another Markdown editor. Trap: Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. Done when: A copy of the vault opened from a different path renders every attachment, tested by copying the fixture vault elsewhere. Front matter keys are documented and any unknown key is preserved untouched on save rather than stripped. The backup command produces a folder that opens as a working vault, exercised in the suite. Opening the folder in a different Markdown editor shows readable cards with working relative image links. Do not build yet: Nothing after this. ### Out of scope (and why) - A sync server, which is the real subscription and is a conflict resolution problem that deserves more than a weekend. - Real time collaborative editing, which needs a merge model and a server and changes the file on disk story completely. - Mobile applications, where capture friction is the entire point and a desktop shortcut does not substitute. - Hosted public publishing of cards, which brings hosting, access control and takedown into a local notebook. ### README must contain - That the Markdown folder is the source of truth and the index can be deleted and rebuilt with one command - The exact filename rules applied, including reserved device names, reserved characters and the path length cap - That links are stored by id with display text, so renaming never breaks one - How external edits are detected and that a clash produces a conflict copy rather than an overwrite - That attachments live inside the vault and are referenced relatively, so a copied folder still works ===== ARCHITECTURE.md ===== # Architecture · Supernotes ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | a desktop application that reads a folder you chose and nothing else | | Storage | One Markdown file per card with front matter, in a folder you pick | the notes have to open in any editor in ten years | | Index | SQLite, derived only, rebuilt from the folder by one command | an index that cannot be thrown away has become a second source of truth | | Identity | A stable id in front matter, independent of the filename and the title | titles change and filenames follow them, links must not | | Writes | Temporary file, flush, atomic replace, with the watcher ignoring its own writes | truncated notes and infinite watch loops are the two classic failures here | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Files | Atomic writes, sanitised names and normalisation. | Any implementation that still passes the filename test list. | | Index | Derived rows and the rebuild command. | Any store, provided rebuild stays a tested command. | | Links | Id based links, display text and backlinks. | Not swappable for title based links, which break on rename. | | Watcher | External change detection and conflict copies. | Any watcher, provided own writes are ignored by content hash. | | Vault | Attachments, front matter and portability. | Any layout where attachments stay inside and paths stay relative. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VAULT_PATH` · required · The folder of Markdown files. This is all of the user's data. - `INDEX_PATH` · required · Derived only. Deleting it must cost nothing but time. - `MAX_FILENAME_LENGTH` · required · Leaves headroom under the platform path limit including the vault prefix. - `UNICODE_NORMALISATION` · required · Applied on write so one accented title cannot become two cards. - `CONFLICT_SUFFIX` · required · Appended to the copy kept when an external edit clashes. - `WATCH_DEBOUNCE_MS` · required · Coalesces editor save storms. It is not how own writes are ignored. ## 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 · Supernotes 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 card with front matter, in a folder you pick, SQLite, derived only, rebuilt from the folder by one command, A stable id in front matter, independent of the filename and the title, Temporary file, flush, atomic replace, with the watcher ignoring its own writes. - 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 - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. - Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. ===== MILESTONES.md ===== # Delivery milestones · Supernotes Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files are the truth, the index is disposable Deleting the database and rebuilding loses nothing. ### Steps 1. Write the file before touching the index The order is the guarantee: a crash between the two costs an index entry, not a note. Files: `src/cards/write.ts` 2. Generate ids independent of the title Because the title is the thing most likely to change. Files: `src/cards/id.ts` 3. Make rebuild a tested command If rebuild is not in the suite, the index quietly becomes load bearing. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] Rebuild from the folder restores everything - [ ] Cards open correctly in a plain text editor - [ ] Ids are not derived from titles - [ ] Search works from a freshly rebuilt index ### Watch out - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. ## M2 · Names that survive every filesystem A card title can contain anything and the filename still works everywhere. ### Steps 1. Sanitise against the documented rules Reserved characters, reserved device names, trailing space and period, path length. Files: `src/files/sanitise.ts`, `tests/filenames.test.ts` 2. Normalise Unicode on write Otherwise one accented title becomes two cards after it crosses a filesystem. Files: `src/files/sanitise.ts` 3. Keep the real title in front matter The filename is a convenience; the title is the data. Files: `src/cards/write.ts` ### Done when - [ ] Reserved names and characters all produce valid filenames - [ ] Case only title differences do not collide - [ ] Accented titles are normalised on write - [ ] Paths stay within the configured length limit ### Watch out - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. ## M3 · Links that survive a rename Renaming a card never breaks a link to it. ### Steps 1. Link by id with display text Two pieces of information, and only one of them is allowed to change. Files: `src/links/parse.ts` 2. Render unresolved links as broken Visibly, with the id, so it can be repaired rather than guessed at. Files: `src/links/render.tsx` 3. Derive backlinks from the index Derived means a rebuild fixes them, which is the point of the index. Files: `src/links/backlinks.ts`, `tests/backlinks.test.ts` ### Done when - [ ] Renaming keeps every link working - [ ] Links to deleted cards render as broken with the id - [ ] Backlinks match a hand count on a fixture - [ ] Rename updates display text only ### Watch out - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. ## M4 · Edits from outside, without losing one A card changed in another editor is picked up and never overwritten. ### Steps 1. Ignore your own writes by content hash Not by a timer, which is how real edits get eaten. Files: `src/watch/index.ts` 2. Keep both sides as a conflict copy Two files and a comparison beats a merge that guessed. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Write atomically, always Temporary file, flush, replace, for every single save. Files: `src/files/atomic.ts` ### Done when - [ ] External edits produce a conflict copy with both versions - [ ] Own writes do not cause redundant re-indexing - [ ] Externally deleted files are marked missing - [ ] A crash mid save never truncates a card ### Watch out - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ## M5 · The folder already is the export (production only) Someone can open the notes in another tool tomorrow and lose nothing. ### Steps 1. Keep attachments inside the vault Relative paths are the difference between a folder and a folder that only works here. Files: `src/attachments.ts` 2. Preserve unknown front matter keys Another tool's keys are somebody's data, so do not tidy them away. Files: `src/cards/frontmatter.ts` 3. Test the restore by opening the copy A backup you have not opened is a folder of unverified bytes. Files: `src/backup.ts`, `tests/restore-vault.test.ts` ### Done when - [ ] A copied vault renders attachments from a new path - [ ] Unknown front matter keys survive a save - [ ] The backup opens as a working vault - [ ] The folder reads correctly in another Markdown editor ### Watch out - Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. ===== OPERATIONS.md ===== # Operations · Supernotes ## Backup Copy the vault folder; that is the entire backup, and the index is explicitly not part of it. Keep one copy off the machine and open it occasionally, because an unopened backup is an assumption. ## Restore Copy the folder back, delete the index, run rebuild, and confirm the card count matches the file count. If the counts differ, a filename rule rejected something, and the rebuild log names it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the suite is the monitoring: rebuild from the folder, the filename test list, backlink counts on a fixture, and a conflict copy produced under a simulated external edit. ## Incident checklist The incidents are a lost edit and an unopenable folder. A lost edit means the watcher ignored an external change it should have kept; an unopenable folder means a filename slipped past the sanitiser onto a stricter filesystem. 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 - [ ] Rebuild from the folder is confirmed lossless - [ ] Every reserved name and character is confirmed handled - [ ] Renaming is confirmed to keep links working - [ ] An external edit is confirmed to produce a conflict copy - [ ] A copied vault is confirmed to render attachments ## Launch constraint Do not market omitted Supernotes 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 folder of Markdown files. This is all of the user's data. VAULT_PATH=~/Notes # Required. Derived only. Deleting it must cost nothing but time. INDEX_PATH=~/.local/share/cards/index.db # Required. Leaves headroom under the platform path limit including the vault prefix. MAX_FILENAME_LENGTH=120 # Required. Applied on write so one accented title cannot become two cards. UNICODE_NORMALISATION=NFC # Required. Appended to the copy kept when an external edit clashes. CONFLICT_SUFFIX=.conflict # Required. Coalesces editor save storms. It is not how own writes are ignored. WATCH_DEBOUNCE_MS=200
# Supernotes · indie build A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Estimated effort: **weekend**. 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 | a desktop application that reads a folder you chose and nothing else | | Storage | One Markdown file per card with front matter, in a folder you pick | the notes have to open in any editor in ten years | | Index | SQLite, derived only, rebuilt from the folder by one command | an index that cannot be thrown away has become a second source of truth | | Identity | A stable id in front matter, independent of the filename and the title | titles change and filenames follow them, links must not | | Writes | Temporary file, flush, atomic replace, with the watcher ignoring its own writes | truncated notes and infinite watch loops are the two classic failures here | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 and a Rust toolchain** · free - Why: Tauri needs both, and finding out during the first build is an avoidable hour. - Get it: nodejs.org and rustup.rs, plus the Tauri prerequisites for your platform. - Verify: A fresh Tauri template builds and opens a window - [ ] **A folder of real notes to test against** · free - Why: Filename sanitising, link resolution and rebuild all behave differently on two hundred real cards than on five invented ones. - Get it: Export an existing notebook to Markdown, or copy a folder of notes you already keep. - Verify: The folder indexes without errors and the card count matches the file count - [ ] **Access to a second operating system** · free - Why: The filename rules only prove themselves on a filesystem with different rules from the one you develop on, and a virtual machine is enough. - Get it: A Windows or macOS virtual machine, or a colleague willing to copy a folder and open it. - Verify: The fixture vault copies across and every card opens - [ ] **A second Markdown editor** · free - Why: The claim that the folder is portable is only true if another tool renders it, and that is a five minute check nobody does unless it is a prerequisite. - Get it: Any Markdown editor that opens a folder. - Verify: Cards, front matter and relative images all render in it ## 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: - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. - real-time collaboration, mobile polish, sync, and card-based workflow refinement - frictionless mobile capture - real-time team editing - hosted publishing - proprietary AI memory If one of those is essential to you, that is the reason to keep paying for Supernotes, and the README should say so rather than pretend.
# Build brief · Supernotes 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 local card notebook whose Markdown folder stays readable and usable without it to replace Supernotes. 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 · a desktop application that reads a folder you chose and nothing else - Storage: One Markdown file per card with front matter, in a folder you pick · the notes have to open in any editor in ten years - Index: SQLite, derived only, rebuilt from the folder by one command · an index that cannot be thrown away has become a second source of truth - Identity: A stable id in front matter, independent of the filename and the title · titles change and filenames follow them, links must not - Writes: Temporary file, flush, atomic replace, with the watcher ignoring its own writes · truncated notes and infinite watch loops are the two classic failures here ### Data model (create this before Phase 1) - card file front matter(id, title, tags, created, updated, pinned) · the file is the record and everything else is derived from it - cards(id, path, title, tags_json, created_at, updated_at, content_hash, missing_since) · an index row, rebuildable at any time - links(from_id, to_id, display_text, ordinal, resolved) · unresolved links are kept and shown rather than dropped - attachments(card_id, relative_path, sha256, bytes) · stored beside the cards and referenced by relative path only - conflicts(id, card_id, path, detected_at, resolved_at, kept) · an external edit that clashes becomes a second file, never an overwrite - index_state(rebuilt_at, cards_indexed, schema_version) · so you can see when the derived data was last derived - Every table except the front matter can be deleted and rebuilt, which is the test of whether the files really are the source of truth. ### Phase 1 · Files are the truth, the index is disposable Build: Store each card as a Markdown file with front matter carrying a stable id. Index titles, tags, links and timestamps into SQLite. Provide a rebuild command that discards the index and reconstructs it from the folder, and run it in the test suite. Trap: Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. Done when: Deleting the index and running rebuild restores every card, tag, link and title, exercised in the suite. Creating a card writes a Markdown file that opens correctly in a plain text editor, front matter included. The id in front matter is generated once and never derived from the title. Search returns results from the rebuilt index with no reference to the old one. Do not build yet: No filename rules, backlinks, watching or attachments. ### Phase 2 · Names that survive every filesystem Build: Derive filenames from titles through a sanitiser: strip the reserved characters, refuse the reserved device names, trim trailing spaces and periods, normalise Unicode to one form, cap the length with room for the folder path, and resolve collisions case insensitively. Trap: Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. Done when: A card titled with each reserved device name, each reserved character, and a trailing period produces a valid filename, tested case by case. Two cards whose titles differ only in case produce two distinct files that do not collide on a case insensitive filesystem. Titles containing accented characters are normalised to one form on write so the same card is not seen as two after a sync. Generated paths stay within the configured maximum length including the folder prefix. Do not build yet: No backlinks, watching, attachments or export yet. ### Phase 3 · Links that survive a rename Build: Write links by id with display text alongside. Resolve them at render time from the index. Renaming a card updates the display text in linking cards but not the target. Show unresolved links as visibly broken with the id they point at, and list backlinks per card. Trap: Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. Done when: Renaming a card leaves every link to it working, tested across three linking cards. A link to a deleted card renders as broken with the id shown rather than as plain text. Backlinks for a card are derived from the index and match a hand count on a fixture folder. Link display text updates on rename without rewriting the target id. Do not build yet: No watching, conflicts, attachments or export yet. ### Phase 4 · Edits from outside, without losing one Build: Watch the folder, comparing modification time and content hash, and ignore the writes the application itself just made. When the file changed underneath an open editor, keep both: write a conflict copy beside the card and show a comparison. Never save over unseen content. Trap: A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. Done when: Editing a card in another editor while it is open produces a conflict copy and a comparison, with both versions intact, tested. The application's own writes never trigger a re-index of unchanged content, verified by counting index writes during a save. A file deleted outside is marked missing rather than removed from the index. A crash during a save leaves either the old file or the new one, never a truncated one. Do not build yet: No attachments, export or backup drill yet. ### Phase 5 · The folder already is the export Build: Store attachments beside the cards and reference them by relative path. Keep front matter to a documented, small set of keys. Add a backup command that copies the folder and a restore drill that opens the copy as a fresh vault. Confirm the folder renders in another Markdown editor. Trap: Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. Done when: A copy of the vault opened from a different path renders every attachment, tested by copying the fixture vault elsewhere. Front matter keys are documented and any unknown key is preserved untouched on save rather than stripped. The backup command produces a folder that opens as a working vault, exercised in the suite. Opening the folder in a different Markdown editor shows readable cards with working relative image links. Do not build yet: Nothing after this. ### Out of scope (and why) - A sync server, which is the real subscription and is a conflict resolution problem that deserves more than a weekend. - Real time collaborative editing, which needs a merge model and a server and changes the file on disk story completely. - Mobile applications, where capture friction is the entire point and a desktop shortcut does not substitute. - Hosted public publishing of cards, which brings hosting, access control and takedown into a local notebook. ### README must contain - That the Markdown folder is the source of truth and the index can be deleted and rebuilt with one command - The exact filename rules applied, including reserved device names, reserved characters and the path length cap - That links are stored by id with display text, so renaming never breaks one - How external edits are detected and that a clash produces a conflict copy rather than an overwrite - That attachments live inside the vault and are referenced relatively, so a copied folder still works
# Agent instructions · Supernotes 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 card with front matter, in a folder you pick, SQLite, derived only, rebuilt from the folder by one command, A stable id in front matter, independent of the filename and the title, Temporary file, flush, atomic replace, with the watcher ignoring its own writes. 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 - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly.
# Build plan · Supernotes A card based notebook whose hard problems are keeping files as the only source of truth with a disposable index, naming those files so they survive every filesystem your notes will ever touch, and handling edits made outside the application without ever overwriting one. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Files are the truth, the index is disposable Deleting the database and rebuilding loses nothing. ### Steps 1. Write the file before touching the index The order is the guarantee: a crash between the two costs an index entry, not a note. Files: `src/cards/write.ts` 2. Generate ids independent of the title Because the title is the thing most likely to change. Files: `src/cards/id.ts` 3. Make rebuild a tested command If rebuild is not in the suite, the index quietly becomes load bearing. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] Rebuild from the folder restores everything - [ ] Cards open correctly in a plain text editor - [ ] Ids are not derived from titles - [ ] Search works from a freshly rebuilt index ### Watch out - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. ## Phase 2 · Names that survive every filesystem A card title can contain anything and the filename still works everywhere. ### Steps 1. Sanitise against the documented rules Reserved characters, reserved device names, trailing space and period, path length. Files: `src/files/sanitise.ts`, `tests/filenames.test.ts` 2. Normalise Unicode on write Otherwise one accented title becomes two cards after it crosses a filesystem. Files: `src/files/sanitise.ts` 3. Keep the real title in front matter The filename is a convenience; the title is the data. Files: `src/cards/write.ts` ### Done when - [ ] Reserved names and characters all produce valid filenames - [ ] Case only title differences do not collide - [ ] Accented titles are normalised on write - [ ] Paths stay within the configured length limit ### Watch out - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. ## Phase 3 · Links that survive a rename Renaming a card never breaks a link to it. ### Steps 1. Link by id with display text Two pieces of information, and only one of them is allowed to change. Files: `src/links/parse.ts` 2. Render unresolved links as broken Visibly, with the id, so it can be repaired rather than guessed at. Files: `src/links/render.tsx` 3. Derive backlinks from the index Derived means a rebuild fixes them, which is the point of the index. Files: `src/links/backlinks.ts`, `tests/backlinks.test.ts` ### Done when - [ ] Renaming keeps every link working - [ ] Links to deleted cards render as broken with the id - [ ] Backlinks match a hand count on a fixture - [ ] Rename updates display text only ### Watch out - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. ## Phase 4 · Edits from outside, without losing one A card changed in another editor is picked up and never overwritten. ### Steps 1. Ignore your own writes by content hash Not by a timer, which is how real edits get eaten. Files: `src/watch/index.ts` 2. Keep both sides as a conflict copy Two files and a comparison beats a merge that guessed. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Write atomically, always Temporary file, flush, replace, for every single save. Files: `src/files/atomic.ts` ### Done when - [ ] External edits produce a conflict copy with both versions - [ ] Own writes do not cause redundant re-indexing - [ ] Externally deleted files are marked missing - [ ] A crash mid save never truncates a card ### Watch out - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ## Not in this build - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. ## After v1, if you want it - Daily notes, which are a card template and a date in the filename and nothing more - Saved searches as cards, so a collection is a note rather than a feature - Sync through a folder service you already pay for, once conflict copies have proven themselves
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder of Markdown files. This is all of the user's data. VAULT_PATH=~/Notes # Required. Derived only. Deleting it must cost nothing but time. INDEX_PATH=~/.local/share/cards/index.db # Required. Leaves headroom under the platform path limit including the vault prefix. MAX_FILENAME_LENGTH=120 # Required. Applied on write so one accented title cannot become two cards. UNICODE_NORMALISATION=NFC # Required. Appended to the copy kept when an external edit clashes. CONFLICT_SUFFIX=.conflict # Required. Coalesces editor save storms. It is not how own writes are ignored. WATCH_DEBOUNCE_MS=200
# Supernotes · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Supernotes, capture small linked cards, tag them, and review or share focused collections. The hard boundary is real-time collaboration, mobile polish, sync, and card-based workflow refinement, plus sync, collaboration, and capture polish. ## Product outcome A notebook whose folder of Markdown files is the whole product, where the index can be deleted without loss, filenames work on every filesystem, links survive renames, and an edit made in another editor is never overwritten. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - macOS, Windows, or Linux - local folder access - optional sync folder chosen by the user ## Explicit non-goals for v1 - A sync server. - Real time collaborative editing. - Mobile applications. - Hosted publishing of cards. - real-time collaboration, mobile polish, sync, and card-based workflow refinement - frictionless mobile capture - real-time team editing - hosted publishing - proprietary AI memory ## Success criteria - Rebuild from the folder is confirmed lossless - Every reserved name and character is confirmed handled - Renaming is confirmed to keep links working - An external edit is confirmed to produce a conflict copy - A copied vault is confirmed to render attachments
# Build brief · Supernotes 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 local card notebook whose Markdown folder stays readable and usable without it to replace Supernotes. 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 · a desktop application that reads a folder you chose and nothing else - Storage: One Markdown file per card with front matter, in a folder you pick · the notes have to open in any editor in ten years - Index: SQLite, derived only, rebuilt from the folder by one command · an index that cannot be thrown away has become a second source of truth - Identity: A stable id in front matter, independent of the filename and the title · titles change and filenames follow them, links must not - Writes: Temporary file, flush, atomic replace, with the watcher ignoring its own writes · truncated notes and infinite watch loops are the two classic failures here ### Data model (create this before Phase 1) - card file front matter(id, title, tags, created, updated, pinned) · the file is the record and everything else is derived from it - cards(id, path, title, tags_json, created_at, updated_at, content_hash, missing_since) · an index row, rebuildable at any time - links(from_id, to_id, display_text, ordinal, resolved) · unresolved links are kept and shown rather than dropped - attachments(card_id, relative_path, sha256, bytes) · stored beside the cards and referenced by relative path only - conflicts(id, card_id, path, detected_at, resolved_at, kept) · an external edit that clashes becomes a second file, never an overwrite - index_state(rebuilt_at, cards_indexed, schema_version) · so you can see when the derived data was last derived - Every table except the front matter can be deleted and rebuilt, which is the test of whether the files really are the source of truth. ### Phase 1 · Files are the truth, the index is disposable Build: Store each card as a Markdown file with front matter carrying a stable id. Index titles, tags, links and timestamps into SQLite. Provide a rebuild command that discards the index and reconstructs it from the folder, and run it in the test suite. Trap: Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. Done when: Deleting the index and running rebuild restores every card, tag, link and title, exercised in the suite. Creating a card writes a Markdown file that opens correctly in a plain text editor, front matter included. The id in front matter is generated once and never derived from the title. Search returns results from the rebuilt index with no reference to the old one. Do not build yet: No filename rules, backlinks, watching or attachments. ### Phase 2 · Names that survive every filesystem Build: Derive filenames from titles through a sanitiser: strip the reserved characters, refuse the reserved device names, trim trailing spaces and periods, normalise Unicode to one form, cap the length with room for the folder path, and resolve collisions case insensitively. Trap: Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. Done when: A card titled with each reserved device name, each reserved character, and a trailing period produces a valid filename, tested case by case. Two cards whose titles differ only in case produce two distinct files that do not collide on a case insensitive filesystem. Titles containing accented characters are normalised to one form on write so the same card is not seen as two after a sync. Generated paths stay within the configured maximum length including the folder prefix. Do not build yet: No backlinks, watching, attachments or export yet. ### Phase 3 · Links that survive a rename Build: Write links by id with display text alongside. Resolve them at render time from the index. Renaming a card updates the display text in linking cards but not the target. Show unresolved links as visibly broken with the id they point at, and list backlinks per card. Trap: Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. Done when: Renaming a card leaves every link to it working, tested across three linking cards. A link to a deleted card renders as broken with the id shown rather than as plain text. Backlinks for a card are derived from the index and match a hand count on a fixture folder. Link display text updates on rename without rewriting the target id. Do not build yet: No watching, conflicts, attachments or export yet. ### Phase 4 · Edits from outside, without losing one Build: Watch the folder, comparing modification time and content hash, and ignore the writes the application itself just made. When the file changed underneath an open editor, keep both: write a conflict copy beside the card and show a comparison. Never save over unseen content. Trap: A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. Done when: Editing a card in another editor while it is open produces a conflict copy and a comparison, with both versions intact, tested. The application's own writes never trigger a re-index of unchanged content, verified by counting index writes during a save. A file deleted outside is marked missing rather than removed from the index. A crash during a save leaves either the old file or the new one, never a truncated one. Do not build yet: No attachments, export or backup drill yet. ### Phase 5 · The folder already is the export Build: Store attachments beside the cards and reference them by relative path. Keep front matter to a documented, small set of keys. Add a backup command that copies the folder and a restore drill that opens the copy as a fresh vault. Confirm the folder renders in another Markdown editor. Trap: Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path. Done when: A copy of the vault opened from a different path renders every attachment, tested by copying the fixture vault elsewhere. Front matter keys are documented and any unknown key is preserved untouched on save rather than stripped. The backup command produces a folder that opens as a working vault, exercised in the suite. Opening the folder in a different Markdown editor shows readable cards with working relative image links. Do not build yet: Nothing after this. ### Out of scope (and why) - A sync server, which is the real subscription and is a conflict resolution problem that deserves more than a weekend. - Real time collaborative editing, which needs a merge model and a server and changes the file on disk story completely. - Mobile applications, where capture friction is the entire point and a desktop shortcut does not substitute. - Hosted public publishing of cards, which brings hosting, access control and takedown into a local notebook. ### README must contain - That the Markdown folder is the source of truth and the index can be deleted and rebuilt with one command - The exact filename rules applied, including reserved device names, reserved characters and the path length cap - That links are stored by id with display text, so renaming never breaks one - How external edits are detected and that a clash produces a conflict copy rather than an overwrite - That attachments live inside the vault and are referenced relatively, so a copied folder still works
# Architecture · Supernotes ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript | a desktop application that reads a folder you chose and nothing else | | Storage | One Markdown file per card with front matter, in a folder you pick | the notes have to open in any editor in ten years | | Index | SQLite, derived only, rebuilt from the folder by one command | an index that cannot be thrown away has become a second source of truth | | Identity | A stable id in front matter, independent of the filename and the title | titles change and filenames follow them, links must not | | Writes | Temporary file, flush, atomic replace, with the watcher ignoring its own writes | truncated notes and infinite watch loops are the two classic failures here | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Files | Atomic writes, sanitised names and normalisation. | Any implementation that still passes the filename test list. | | Index | Derived rows and the rebuild command. | Any store, provided rebuild stays a tested command. | | Links | Id based links, display text and backlinks. | Not swappable for title based links, which break on rename. | | Watcher | External change detection and conflict copies. | Any watcher, provided own writes are ignored by content hash. | | Vault | Attachments, front matter and portability. | Any layout where attachments stay inside and paths stay relative. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VAULT_PATH` · required · The folder of Markdown files. This is all of the user's data. - `INDEX_PATH` · required · Derived only. Deleting it must cost nothing but time. - `MAX_FILENAME_LENGTH` · required · Leaves headroom under the platform path limit including the vault prefix. - `UNICODE_NORMALISATION` · required · Applied on write so one accented title cannot become two cards. - `CONFLICT_SUFFIX` · required · Appended to the copy kept when an external edit clashes. - `WATCH_DEBOUNCE_MS` · required · Coalesces editor save storms. It is not how own writes are ignored. ## 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 · Supernotes 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 card with front matter, in a folder you pick, SQLite, derived only, rebuilt from the folder by one command, A stable id in front matter, independent of the filename and the title, Temporary file, flush, atomic replace, with the watcher ignoring its own writes. - 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 - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. - Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path.
# Delivery milestones · Supernotes Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files are the truth, the index is disposable Deleting the database and rebuilding loses nothing. ### Steps 1. Write the file before touching the index The order is the guarantee: a crash between the two costs an index entry, not a note. Files: `src/cards/write.ts` 2. Generate ids independent of the title Because the title is the thing most likely to change. Files: `src/cards/id.ts` 3. Make rebuild a tested command If rebuild is not in the suite, the index quietly becomes load bearing. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] Rebuild from the folder restores everything - [ ] Cards open correctly in a plain text editor - [ ] Ids are not derived from titles - [ ] Search works from a freshly rebuilt index ### Watch out - Keeping the card body in SQLite and writing Markdown only on export means the export is a feature that can break, so the day it breaks the notes are inside a database file the person cannot read. Write the file first, index second, and treat the index as a cache you are willing to delete at any moment. ## M2 · Names that survive every filesystem A card title can contain anything and the filename still works everywhere. ### Steps 1. Sanitise against the documented rules Reserved characters, reserved device names, trailing space and period, path length. Files: `src/files/sanitise.ts`, `tests/filenames.test.ts` 2. Normalise Unicode on write Otherwise one accented title becomes two cards after it crosses a filesystem. Files: `src/files/sanitise.ts` 3. Keep the real title in front matter The filename is a convenience; the title is the data. Files: `src/cards/write.ts` ### Done when - [ ] Reserved names and characters all produce valid filenames - [ ] Case only title differences do not collide - [ ] Accented titles are normalised on write - [ ] Paths stay within the configured length limit ### Watch out - Naming files straight from card titles produces notes that are fine on your laptop and unopenable the moment the folder touches Windows, which forbids the characters < > : " / \ | ? *, treats CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 as devices even with an extension added, refuses trailing spaces and periods, and historically caps a path at 260 characters. Sanitise on write, keep the real title in front matter, and test the whole list. ## M3 · Links that survive a rename Renaming a card never breaks a link to it. ### Steps 1. Link by id with display text Two pieces of information, and only one of them is allowed to change. Files: `src/links/parse.ts` 2. Render unresolved links as broken Visibly, with the id, so it can be repaired rather than guessed at. Files: `src/links/render.tsx` 3. Derive backlinks from the index Derived means a rebuild fixes them, which is the point of the index. Files: `src/links/backlinks.ts`, `tests/backlinks.test.ts` ### Done when - [ ] Renaming keeps every link working - [ ] Links to deleted cards render as broken with the id - [ ] Backlinks match a hand count on a fixture - [ ] Rename updates display text only ### Watch out - Linking by title means every rename silently breaks every link that pointed at the old one, and because links are only followed occasionally the damage is discovered months later across dozens of cards. Link by id, keep the title as display text, and let a rename update the text without touching the target. ## M4 · Edits from outside, without losing one A card changed in another editor is picked up and never overwritten. ### Steps 1. Ignore your own writes by content hash Not by a timer, which is how real edits get eaten. Files: `src/watch/index.ts` 2. Keep both sides as a conflict copy Two files and a comparison beats a merge that guessed. Files: `src/conflicts.ts`, `tests/conflict.test.ts` 3. Write atomically, always Temporary file, flush, replace, for every single save. Files: `src/files/atomic.ts` ### Done when - [ ] External edits produce a conflict copy with both versions - [ ] Own writes do not cause redundant re-indexing - [ ] Externally deleted files are marked missing - [ ] A crash mid save never truncates a card ### Watch out - A watcher that reacts to its own writes loops forever and rewrites files until something gives up, and the usual quick fix of ignoring events for a second turns into losing a real external edit that arrived inside that second. Track the hash you just wrote and ignore an event only when the content matches it exactly. ## M5 · The folder already is the export (production only) Someone can open the notes in another tool tomorrow and lose nothing. ### Steps 1. Keep attachments inside the vault Relative paths are the difference between a folder and a folder that only works here. Files: `src/attachments.ts` 2. Preserve unknown front matter keys Another tool's keys are somebody's data, so do not tidy them away. Files: `src/cards/frontmatter.ts` 3. Test the restore by opening the copy A backup you have not opened is a folder of unverified bytes. Files: `src/backup.ts`, `tests/restore-vault.test.ts` ### Done when - [ ] A copied vault renders attachments from a new path - [ ] Unknown front matter keys survive a save - [ ] The backup opens as a working vault - [ ] The folder reads correctly in another Markdown editor ### Watch out - Keeping attachments in an application support directory and referencing them by absolute path means the folder copied to another machine renders every image as a broken box, and the person only finds out when they have already moved. Store attachments inside the vault and reference them relatively, then test by opening a copied folder from a different path.
# Operations · Supernotes ## Backup Copy the vault folder; that is the entire backup, and the index is explicitly not part of it. Keep one copy off the machine and open it occasionally, because an unopened backup is an assumption. ## Restore Copy the folder back, delete the index, run rebuild, and confirm the card count matches the file count. If the counts differ, a filename rule rejected something, and the rebuild log names it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool the suite is the monitoring: rebuild from the folder, the filename test list, backlink counts on a fixture, and a conflict copy produced under a simulated external edit. ## Incident checklist The incidents are a lost edit and an unopenable folder. A lost edit means the watcher ignored an external change it should have kept; an unopenable folder means a filename slipped past the sanitiser onto a stricter filesystem. 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 - [ ] Rebuild from the folder is confirmed lossless - [ ] Every reserved name and character is confirmed handled - [ ] Renaming is confirmed to keep links working - [ ] An external edit is confirmed to produce a conflict copy - [ ] A copied vault is confirmed to render attachments ## Launch constraint Do not market omitted Supernotes 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 folder of Markdown files. This is all of the user's data. VAULT_PATH=~/Notes # Required. Derived only. Deleting it must cost nothing but time. INDEX_PATH=~/.local/share/cards/index.db # Required. Leaves headroom under the platform path limit including the vault prefix. MAX_FILENAME_LENGTH=120 # Required. Applied on write so one accented title cannot become two cards. UNICODE_NORMALISATION=NFC # Required. Appended to the copy kept when an external edit clashes. CONFLICT_SUFFIX=.conflict # Required. Coalesces editor save storms. It is not how own writes are ignored. WATCH_DEBOUNCE_MS=200
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Supernotes because people keep paying when the note system is trusted across every device and disappears into their daily capture habits. The recurring cost buys file watching, conflict handling, indexing, mobile apps, encryption, backups, and migrations, not just the visible interface.
xreal-time collaboration, mobile polish, sync, and card-based workflow refinement
xfrictionless mobile capture
xreal-time team editing
xhosted publishing
xproprietary AI memory
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Supernotes pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $0/user | $0/user | Starts with 100 kept cards; +20 cards per successful referral; cards have a 1,200-character soft limit. |
| unlimited | $10.82/user | $8.12/user | Unlimited cards; full API and integrations; AI features; priority support on yearly billing. |
| unlimited 4-year licence | custom | — | Unlimited creation for 4 years; after expiry, existing cards remain readable, editable and shareable, but no new cards can be created. |
free tier100 kept cards initially; achievements and referrals can raise the cap, with +20 cards per successful referral
billingmonthly + annual + one-time 4-year licence; £72 billed annually; 14-day money-back guarantee
hidden costsApp Store/Play Store prices and taxes can differ by region; student pricing is 50% off yearly or 63% off the 4-year licence; new-card creation stops when a 4-year licence expires unless renewed
verified 2026-08-13 · source ↗
Vibecode Supernotes
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Supernotes replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Supernotes cost?
Supernotes costs about $10.82/month (Unlimited, checked 2026-08-13), which is $129.84 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Supernotes?
Honestly: real-time collaboration, mobile polish, sync, and card-based workflow refinement; frictionless mobile capture; real-time team editing; hosted publishing; proprietary AI memory. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Supernotes?
Yes: Memos (Small Markdown cards, tags and public or private sharing, served from one Docker container.) The prompt is for when you want it exactly your way.