Vibecode Journey
track this build4 phases, 12 steps, beginner friendly0%The visible digital journal loop is buildable, but a credible replacement needs more than the first screen. Journey earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Journey. 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 ===== # Journey · indie build A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. Estimated effort: **multi-day**. 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 | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript | a desktop application that reads and writes a folder you own | | Storage | Markdown files with front matter, media beside them, written atomically | the files are the journal; anything else is a cache | | Index | SQLite with FTS5, rebuildable from the folder in one command | search needs an index, but an index must never be the only copy | | Time | Local wall time plus the zone identifier, both stored, never one alone | an entry written at nine in Tokyo is not the same entry at nine in London | | Media | Copied into the folder, originals never moved or modified | importing a photograph should not be able to lose the photograph | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell and the file handling that must not corrupt an entry. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **SQLite built with FTS5** · free - Why: Search is the feature that makes a long journal usable, and FTS5 is not present in every build. - Get it: Most distributions include it; check rather than assume. - Verify: A create virtual table using fts5 statement succeeds - [ ] **Photographs with and without location metadata** · free - Why: The zone-unknown capture time, the orientation flag and the embedded coordinates are all only testable with real files. - Get it: A few pictures from a phone, plus one with location switched off and one rotated. - Verify: Each behaves as its phase requires - [ ] **Which languages the journal will contain** · free - Why: It decides the FTS5 tokenizer, and changing that later means reindexing after the person has already stopped trusting search. - Get it: Write down every script you expect to write in, including the occasional one. - Verify: The tokenizer choice in the README names them - [ ] **Storage you control for encrypted backups** (optional) · pennies per month, or free on a spare disk - Why: A local-first journal with no offsite copy is one disk failure from nothing. - Get it: Any object storage or removable disk with a key you hold. - Verify: A backup written and restored into an empty install ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. - high-fidelity color, format, and export handling - native mobile clients - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing If one of those is essential to you, that is the reason to keep paying for Journey, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Journey 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-first journal whose entries stay readable without the app that wrote them to replace Journey. 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. ### Rule zero A journal is the one application where the data has to outlive the software, and most of them fail that test quietly. So the entries are plain files on disk in a folder the person can open in any text editor: one Markdown file per entry with its metadata in front matter, media beside it under names that mean something. The database is an index, nothing more. Deleting it and rebuilding it from the folder is a tested operation, not a theory, and it is the proof that the files really are the truth rather than a copy that drifts. The second rule is about what an entry knows. A journal entry can carry a location, a device name and the exact second it was written, and the photographs carry coordinates of their own in their metadata. That is fine while it stays on the machine. The moment anything leaves, through an export, a share or a backup uploaded somewhere, location metadata is stripped unless the person asked for it in that specific action, and the interface says which fields are travelling. ### Stack (fixed, do not substitute) - Runtime: Tauri 2 with React and TypeScript · a desktop application that reads and writes a folder you own - Storage: Markdown files with front matter, media beside them, written atomically · the files are the journal; anything else is a cache - Index: SQLite with FTS5, rebuildable from the folder in one command · search needs an index, but an index must never be the only copy - Time: Local wall time plus the zone identifier, both stored, never one alone · an entry written at nine in Tokyo is not the same entry at nine in London - Media: Copied into the folder, originals never moved or modified · importing a photograph should not be able to lose the photograph ### Data model (create this before Phase 1) - On disk: entries/2026/2026-09-11-0743-<id>.md with YAML front matter, and media/2026/09/<id>-<n>.<ext> - entries(id, path, written_at_local, written_at_utc, timezone, latitude, longitude, place_label, body_sha256, indexed_at) - media(id, entry_id, path, mime, bytes, sha256, taken_at_local, taken_at_zone_known, latitude, longitude, width, height) - tags(entry_id, tag) and links(from_entry, to_entry) · both derived from the file, both rebuildable - revisions(entry_id, ordinal, body_sha256, path, at) · previous versions kept as files under revisions/ - entries_fts(entry_id, body) · an FTS5 table whose tokenizer is a recorded decision, not a default nobody looked at - Every table above can be dropped and rebuilt from the folder, and the rebuild is a test rather than a claim. ### Phase 1 · Files first, index second Build: Write each entry as a Markdown file with front matter holding the identifier, the local time, the zone, tags and any coordinates. Write to a temporary file, flush, then rename over the target, and keep the previous version under revisions/. Build the index by scanning the folder, and provide a rebuild command that deletes the database and reconstructs it from the files alone. Trap: Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. Done when: Deleting the database and running the rebuild reproduces every entry, tag, link and media reference, asserted in the suite against a fixture folder. An entry file opened in a plain text editor is readable and its front matter is valid YAML. Killing the process during a save leaves either the previous version or the complete new one, never a truncated file, tested by injecting a crash. Editing a file outside the app and rescanning updates the index without losing the external edit. Do not build yet: No search, no media import, no maps, no export. ### Phase 2 · Search that works in the language you write in Build: Index entry bodies in FTS5 and choose the tokenizer deliberately. The default unicode61 tokenizer splits on characters that are not letters or numbers, which means a language written without spaces indexes as one enormous token and never matches. The trigram tokenizer gives substring matching and indexed LIKE and GLOB, but any query shorter than three characters matches no rows at all. Pick per the languages in the journal, record the choice in the README, and test both a two-character query and a query in a non-Latin script. Trap: Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. Done when: A search in each script the journal contains returns the right entries, asserted with fixtures in at least two scripts. A query of fewer than three characters behaves by a rule stated in the README and in the interface rather than silently returning nothing. Search over a thousand entries returns in under two hundred milliseconds on the development machine, measured. Reindexing after a tokenizer change is a single command and does not touch the entry files. Do not build yet: No media import, no location handling, no export or backup. ### Phase 3 · Photographs, timestamps and places Build: Copy imported media into the folder, never move or rewrite the original. Read the capture time and coordinates from the file's metadata, and record whether the capture time carried a zone, because the common field does not. Apply the image orientation flag when rendering rather than rotating the stored file. Store the entry's local time and its zone identifier together, and show both when they disagree with the machine's current zone. Trap: Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. Done when: An image whose capture time has no zone is recorded as zone-unknown and never silently assigned the machine's zone, tested with a fixture. A photograph with an orientation flag renders upright while the stored file is byte-identical to the original, verified by checksum. Coordinates from media are stored separately from coordinates the person set on the entry, and both are visible. An entry written in one zone and viewed in another shows the original local time with its zone named. Do not build yet: No revision browser, no export, no backup or sharing. ### Phase 4 · Revisions and a timeline worth trusting Build: Keep each previous body as a file under revisions/ with its hash, and offer a diff and a restore. Let an entry be backdated, storing the written time and the recorded time separately so a memory written today about last March appears in March without pretending it was written then. Build the calendar and the on-this-day view from the entry's own local date, not from a converted instant. Trap: Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. Done when: Restoring a previous revision writes a new file and keeps the revision that was replaced, so nothing is destroyed by a restore. A backdated entry appears on the backdated date and its record of when it was actually written is visible. Entries written at eleven at night in a distant zone group on their own local date, tested with fixtures from three zones. The revisions folder is human-readable and included in the rebuild. Do not build yet: No export, backup or sharing yet. ### Phase 5 · Leaving with everything, and leaking nothing Build: Export the folder as it stands plus a manifest, and prove the export by reimporting it into an empty install and comparing hashes. Strip location metadata from media on any share or export unless that action explicitly asked to keep it, and show which fields are travelling. Offer an encrypted backup to storage the person controls, with a restore exercised in the suite. Bind any local server the app runs to the loopback interface only. Trap: Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. Done when: An export reimported into an empty install reproduces every entry, revision and media file, compared by hash in the suite. A shared or exported image has no location metadata unless that action asked for it, asserted by reading the output file. A backup restore reproduces a working journal, exercised in the suite rather than documented. The share dialog lists exactly which fields leave with the entry. Do not build yet: Nothing after this. ### Out of scope (and why) - Conflict-safe multi-device sync, which is a distributed systems project in its own right and the thing a journal most often loses data to. - Native mobile clients, which double the storage and media work and need their own release process. - Collaborative editing and shared journals, which change the privacy model of every feature above. - Hosted storage you operate, which makes you the custodian of other people's diaries. ### README must contain - That entries are Markdown files with front matter and the database is a rebuildable index - The exact command that deletes the index and rebuilds it from the folder - Which FTS5 tokenizer was chosen, why, and what a query shorter than three characters does - That media is copied unmodified and orientation is applied at render time only - That location metadata is stripped from anything leaving unless that action asked to keep it ===== AGENTS.md ===== # Agent instructions · Journey indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Markdown files with front matter, media beside them, written atomically, SQLite with FTS5, rebuildable from the folder in one command, Local wall time plus the zone identifier, both stored, never one alone, Copied into the folder, originals never moved or modified. 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 - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ===== BUILD_PLAN.md ===== # Build plan · Journey A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. 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 first, index second Entries live as Markdown on disk and the database can be deleted without losing anything. ### Steps 1. Write the file, then index it The order is the whole guarantee; reversing it makes the folder a copy. Files: `src-tauri/src/storage.rs`, `src/entry/save.ts` 2. Rename over the target after a flush A truncated entry file is the one loss this design exists to prevent. Files: `src-tauri/src/storage.rs`, `tests/atomic-write.test.ts` 3. Make the rebuild a tested command An index nobody has rebuilt is an index that cannot be rebuilt. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] A deleted database rebuilds from the folder alone - [ ] Entry files are readable in a plain text editor - [ ] A crash mid-save leaves no truncated file - [ ] An external edit survives a rescan ### Watch out - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. ## Phase 2 · Search that works in the language you write in Full-text search returns the right entries for the scripts and short words the person actually uses. ### Steps 1. Choose the tokenizer against the real languages The default is right for English prose and wrong for anything written without spaces. Files: `src/index/fts.ts`, `docs/search.md` 2. Handle the short query case explicitly Silently returning nothing for a two-character search reads as a broken app. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Test search in two scripts One script in the fixtures means one script that works. Files: `tests/search-scripts.test.ts` ### Done when - [ ] Search works in at least two scripts, asserted - [ ] Short queries follow a stated rule, not silence - [ ] A thousand entries search in under 200 ms - [ ] Reindexing never touches the entry files ### Watch out - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. ## Phase 3 · Photographs, timestamps and places Imported media keeps its own truth and the entry keeps a time that means something. ### Steps 1. Copy media in, never modify the original Verified by comparing checksums before and after import. Files: `src/media/import.ts`, `tests/media-untouched.test.ts` 2. Record whether the capture time carried a zone The usual metadata field does not, and guessing makes a holiday land on the wrong days. Files: `src/media/exif.ts` 3. Rotate at render, not on disk The orientation flag is information, not a defect to be corrected in the file. Files: `src/media/render.tsx` ### Done when - [ ] Zone-unknown capture times stay unknown - [ ] Imported media is byte-identical to the original - [ ] Media and manual coordinates are stored separately - [ ] An entry shows its original local time and zone ### Watch out - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. ## Phase 4 · Revisions and a timeline worth trusting Edits are recoverable and the calendar view never misplaces an entry. ### Steps 1. Keep revisions as files with hashes A revision inside the database contradicts the entire storage rule. Files: `src/entry/revisions.ts` 2. Separate the written date from the recorded date Backdating is honest only when both are kept. Files: `src/entry/dates.ts` 3. Group the timeline by local date Converting to the current zone misfiles every entry written abroad at night. Files: `src/views/timeline.tsx`, `tests/timeline-zones.test.ts` ### Done when - [ ] A restore preserves the replaced revision - [ ] Backdated entries show both dates - [ ] Late-night entries abroad group on their local date - [ ] Revisions are readable and rebuilt into the index ### Watch out - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ## Not in this build - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. ## After v1, if you want it - A read-only viewer that opens the folder with no database at all, which also proves the storage rule - Map clustering over entry coordinates, computed locally from the index - An importer for one other journal's export format, written against real files rather than its documentation ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder that is the journal. Everything else is rebuildable from it. JOURNAL_DIR=~/Documents/Journal # Required. The SQLite index. Safe to delete, because the rebuild is tested. INDEX_PATH=~/.local/share/journal/index.db # Required. Recorded as a decision. Changing it reindexes and touches no entry file. FTS_TOKENIZER=unicode61 # Required. How many previous bodies to keep per entry, as files under revisions/. REVISION_KEEP=50 # Required. The default for anything leaving the machine. Overridden per action, never globally by accident. STRIP_LOCATION_ON_EXPORT=true # Optional. Encrypted backup destination, if you configure one. BACKUP_TARGET=s3://my-bucket/journal # Optional · secret. Encrypts the backup. Lose it and the backup is noise. BACKUP_PASSPHRASE=a long passphrase, redacted
You are building a lean indie version of Journey. 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 ===== # Journey · indie build A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. Estimated effort: **multi-day**. 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 | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript | a desktop application that reads and writes a folder you own | | Storage | Markdown files with front matter, media beside them, written atomically | the files are the journal; anything else is a cache | | Index | SQLite with FTS5, rebuildable from the folder in one command | search needs an index, but an index must never be the only copy | | Time | Local wall time plus the zone identifier, both stored, never one alone | an entry written at nine in Tokyo is not the same entry at nine in London | | Media | Copied into the folder, originals never moved or modified | importing a photograph should not be able to lose the photograph | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell and the file handling that must not corrupt an entry. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **SQLite built with FTS5** · free - Why: Search is the feature that makes a long journal usable, and FTS5 is not present in every build. - Get it: Most distributions include it; check rather than assume. - Verify: A create virtual table using fts5 statement succeeds - [ ] **Photographs with and without location metadata** · free - Why: The zone-unknown capture time, the orientation flag and the embedded coordinates are all only testable with real files. - Get it: A few pictures from a phone, plus one with location switched off and one rotated. - Verify: Each behaves as its phase requires - [ ] **Which languages the journal will contain** · free - Why: It decides the FTS5 tokenizer, and changing that later means reindexing after the person has already stopped trusting search. - Get it: Write down every script you expect to write in, including the occasional one. - Verify: The tokenizer choice in the README names them - [ ] **Storage you control for encrypted backups** (optional) · pennies per month, or free on a spare disk - Why: A local-first journal with no offsite copy is one disk failure from nothing. - Get it: Any object storage or removable disk with a key you hold. - Verify: A backup written and restored into an empty install ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. - high-fidelity color, format, and export handling - native mobile clients - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing If one of those is essential to you, that is the reason to keep paying for Journey, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Journey 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-first journal whose entries stay readable without the app that wrote them to replace Journey. 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. ### Rule zero A journal is the one application where the data has to outlive the software, and most of them fail that test quietly. So the entries are plain files on disk in a folder the person can open in any text editor: one Markdown file per entry with its metadata in front matter, media beside it under names that mean something. The database is an index, nothing more. Deleting it and rebuilding it from the folder is a tested operation, not a theory, and it is the proof that the files really are the truth rather than a copy that drifts. The second rule is about what an entry knows. A journal entry can carry a location, a device name and the exact second it was written, and the photographs carry coordinates of their own in their metadata. That is fine while it stays on the machine. The moment anything leaves, through an export, a share or a backup uploaded somewhere, location metadata is stripped unless the person asked for it in that specific action, and the interface says which fields are travelling. ### Stack (fixed, do not substitute) - Runtime: Tauri 2 with React and TypeScript · a desktop application that reads and writes a folder you own - Storage: Markdown files with front matter, media beside them, written atomically · the files are the journal; anything else is a cache - Index: SQLite with FTS5, rebuildable from the folder in one command · search needs an index, but an index must never be the only copy - Time: Local wall time plus the zone identifier, both stored, never one alone · an entry written at nine in Tokyo is not the same entry at nine in London - Media: Copied into the folder, originals never moved or modified · importing a photograph should not be able to lose the photograph ### Data model (create this before Phase 1) - On disk: entries/2026/2026-09-11-0743-<id>.md with YAML front matter, and media/2026/09/<id>-<n>.<ext> - entries(id, path, written_at_local, written_at_utc, timezone, latitude, longitude, place_label, body_sha256, indexed_at) - media(id, entry_id, path, mime, bytes, sha256, taken_at_local, taken_at_zone_known, latitude, longitude, width, height) - tags(entry_id, tag) and links(from_entry, to_entry) · both derived from the file, both rebuildable - revisions(entry_id, ordinal, body_sha256, path, at) · previous versions kept as files under revisions/ - entries_fts(entry_id, body) · an FTS5 table whose tokenizer is a recorded decision, not a default nobody looked at - Every table above can be dropped and rebuilt from the folder, and the rebuild is a test rather than a claim. ### Phase 1 · Files first, index second Build: Write each entry as a Markdown file with front matter holding the identifier, the local time, the zone, tags and any coordinates. Write to a temporary file, flush, then rename over the target, and keep the previous version under revisions/. Build the index by scanning the folder, and provide a rebuild command that deletes the database and reconstructs it from the files alone. Trap: Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. Done when: Deleting the database and running the rebuild reproduces every entry, tag, link and media reference, asserted in the suite against a fixture folder. An entry file opened in a plain text editor is readable and its front matter is valid YAML. Killing the process during a save leaves either the previous version or the complete new one, never a truncated file, tested by injecting a crash. Editing a file outside the app and rescanning updates the index without losing the external edit. Do not build yet: No search, no media import, no maps, no export. ### Phase 2 · Search that works in the language you write in Build: Index entry bodies in FTS5 and choose the tokenizer deliberately. The default unicode61 tokenizer splits on characters that are not letters or numbers, which means a language written without spaces indexes as one enormous token and never matches. The trigram tokenizer gives substring matching and indexed LIKE and GLOB, but any query shorter than three characters matches no rows at all. Pick per the languages in the journal, record the choice in the README, and test both a two-character query and a query in a non-Latin script. Trap: Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. Done when: A search in each script the journal contains returns the right entries, asserted with fixtures in at least two scripts. A query of fewer than three characters behaves by a rule stated in the README and in the interface rather than silently returning nothing. Search over a thousand entries returns in under two hundred milliseconds on the development machine, measured. Reindexing after a tokenizer change is a single command and does not touch the entry files. Do not build yet: No media import, no location handling, no export or backup. ### Phase 3 · Photographs, timestamps and places Build: Copy imported media into the folder, never move or rewrite the original. Read the capture time and coordinates from the file's metadata, and record whether the capture time carried a zone, because the common field does not. Apply the image orientation flag when rendering rather than rotating the stored file. Store the entry's local time and its zone identifier together, and show both when they disagree with the machine's current zone. Trap: Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. Done when: An image whose capture time has no zone is recorded as zone-unknown and never silently assigned the machine's zone, tested with a fixture. A photograph with an orientation flag renders upright while the stored file is byte-identical to the original, verified by checksum. Coordinates from media are stored separately from coordinates the person set on the entry, and both are visible. An entry written in one zone and viewed in another shows the original local time with its zone named. Do not build yet: No revision browser, no export, no backup or sharing. ### Phase 4 · Revisions and a timeline worth trusting Build: Keep each previous body as a file under revisions/ with its hash, and offer a diff and a restore. Let an entry be backdated, storing the written time and the recorded time separately so a memory written today about last March appears in March without pretending it was written then. Build the calendar and the on-this-day view from the entry's own local date, not from a converted instant. Trap: Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. Done when: Restoring a previous revision writes a new file and keeps the revision that was replaced, so nothing is destroyed by a restore. A backdated entry appears on the backdated date and its record of when it was actually written is visible. Entries written at eleven at night in a distant zone group on their own local date, tested with fixtures from three zones. The revisions folder is human-readable and included in the rebuild. Do not build yet: No export, backup or sharing yet. ### Phase 5 · Leaving with everything, and leaking nothing Build: Export the folder as it stands plus a manifest, and prove the export by reimporting it into an empty install and comparing hashes. Strip location metadata from media on any share or export unless that action explicitly asked to keep it, and show which fields are travelling. Offer an encrypted backup to storage the person controls, with a restore exercised in the suite. Bind any local server the app runs to the loopback interface only. Trap: Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. Done when: An export reimported into an empty install reproduces every entry, revision and media file, compared by hash in the suite. A shared or exported image has no location metadata unless that action asked for it, asserted by reading the output file. A backup restore reproduces a working journal, exercised in the suite rather than documented. The share dialog lists exactly which fields leave with the entry. Do not build yet: Nothing after this. ### Out of scope (and why) - Conflict-safe multi-device sync, which is a distributed systems project in its own right and the thing a journal most often loses data to. - Native mobile clients, which double the storage and media work and need their own release process. - Collaborative editing and shared journals, which change the privacy model of every feature above. - Hosted storage you operate, which makes you the custodian of other people's diaries. ### README must contain - That entries are Markdown files with front matter and the database is a rebuildable index - The exact command that deletes the index and rebuilds it from the folder - Which FTS5 tokenizer was chosen, why, and what a query shorter than three characters does - That media is copied unmodified and orientation is applied at render time only - That location metadata is stripped from anything leaving unless that action asked to keep it ===== AGENTS.md ===== # Agent instructions · Journey indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Markdown files with front matter, media beside them, written atomically, SQLite with FTS5, rebuildable from the folder in one command, Local wall time plus the zone identifier, both stored, never one alone, Copied into the folder, originals never moved or modified. 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 - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ===== BUILD_PLAN.md ===== # Build plan · Journey A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. 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 first, index second Entries live as Markdown on disk and the database can be deleted without losing anything. ### Steps 1. Write the file, then index it The order is the whole guarantee; reversing it makes the folder a copy. Files: `src-tauri/src/storage.rs`, `src/entry/save.ts` 2. Rename over the target after a flush A truncated entry file is the one loss this design exists to prevent. Files: `src-tauri/src/storage.rs`, `tests/atomic-write.test.ts` 3. Make the rebuild a tested command An index nobody has rebuilt is an index that cannot be rebuilt. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] A deleted database rebuilds from the folder alone - [ ] Entry files are readable in a plain text editor - [ ] A crash mid-save leaves no truncated file - [ ] An external edit survives a rescan ### Watch out - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. ## Phase 2 · Search that works in the language you write in Full-text search returns the right entries for the scripts and short words the person actually uses. ### Steps 1. Choose the tokenizer against the real languages The default is right for English prose and wrong for anything written without spaces. Files: `src/index/fts.ts`, `docs/search.md` 2. Handle the short query case explicitly Silently returning nothing for a two-character search reads as a broken app. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Test search in two scripts One script in the fixtures means one script that works. Files: `tests/search-scripts.test.ts` ### Done when - [ ] Search works in at least two scripts, asserted - [ ] Short queries follow a stated rule, not silence - [ ] A thousand entries search in under 200 ms - [ ] Reindexing never touches the entry files ### Watch out - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. ## Phase 3 · Photographs, timestamps and places Imported media keeps its own truth and the entry keeps a time that means something. ### Steps 1. Copy media in, never modify the original Verified by comparing checksums before and after import. Files: `src/media/import.ts`, `tests/media-untouched.test.ts` 2. Record whether the capture time carried a zone The usual metadata field does not, and guessing makes a holiday land on the wrong days. Files: `src/media/exif.ts` 3. Rotate at render, not on disk The orientation flag is information, not a defect to be corrected in the file. Files: `src/media/render.tsx` ### Done when - [ ] Zone-unknown capture times stay unknown - [ ] Imported media is byte-identical to the original - [ ] Media and manual coordinates are stored separately - [ ] An entry shows its original local time and zone ### Watch out - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. ## Phase 4 · Revisions and a timeline worth trusting Edits are recoverable and the calendar view never misplaces an entry. ### Steps 1. Keep revisions as files with hashes A revision inside the database contradicts the entire storage rule. Files: `src/entry/revisions.ts` 2. Separate the written date from the recorded date Backdating is honest only when both are kept. Files: `src/entry/dates.ts` 3. Group the timeline by local date Converting to the current zone misfiles every entry written abroad at night. Files: `src/views/timeline.tsx`, `tests/timeline-zones.test.ts` ### Done when - [ ] A restore preserves the replaced revision - [ ] Backdated entries show both dates - [ ] Late-night entries abroad group on their local date - [ ] Revisions are readable and rebuilt into the index ### Watch out - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ## Not in this build - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. ## After v1, if you want it - A read-only viewer that opens the folder with no database at all, which also proves the storage rule - Map clustering over entry coordinates, computed locally from the index - An importer for one other journal's export format, written against real files rather than its documentation ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder that is the journal. Everything else is rebuildable from it. JOURNAL_DIR=~/Documents/Journal # Required. The SQLite index. Safe to delete, because the rebuild is tested. INDEX_PATH=~/.local/share/journal/index.db # Required. Recorded as a decision. Changing it reindexes and touches no entry file. FTS_TOKENIZER=unicode61 # Required. How many previous bodies to keep per entry, as files under revisions/. REVISION_KEEP=50 # Required. The default for anything leaving the machine. Overridden per action, never globally by accident. STRIP_LOCATION_ON_EXPORT=true # Optional. Encrypted backup destination, if you configure one. BACKUP_TARGET=s3://my-bucket/journal # Optional · secret. Encrypts the backup. Lose it and the backup is noise. BACKUP_PASSPHRASE=a long passphrase, redacted
You are building a production product version of Journey. 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 ===== # Journey · product brief ## Problem The visible digital journal loop is buildable, but a credible replacement needs more than the first screen. Journey earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A journal you could still read in twenty years with a text editor, whose search works in the languages you write in, whose photographs keep their own timestamps, and which does not post your coordinates to whoever you shared an entry with. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. - high-fidelity color, format, and export handling - native mobile clients - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing ## Success criteria - A deleted index is confirmed to rebuild from the folder alone - A crash mid-save is confirmed to leave no truncated file - Search is confirmed to work in every script in the fixtures - Imported media is confirmed byte-identical to the original - Exported media is confirmed to carry no location unless asked ===== BRIEF.md ===== # Build brief · Journey 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-first journal whose entries stay readable without the app that wrote them to replace Journey. 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. ### Rule zero A journal is the one application where the data has to outlive the software, and most of them fail that test quietly. So the entries are plain files on disk in a folder the person can open in any text editor: one Markdown file per entry with its metadata in front matter, media beside it under names that mean something. The database is an index, nothing more. Deleting it and rebuilding it from the folder is a tested operation, not a theory, and it is the proof that the files really are the truth rather than a copy that drifts. The second rule is about what an entry knows. A journal entry can carry a location, a device name and the exact second it was written, and the photographs carry coordinates of their own in their metadata. That is fine while it stays on the machine. The moment anything leaves, through an export, a share or a backup uploaded somewhere, location metadata is stripped unless the person asked for it in that specific action, and the interface says which fields are travelling. ### Stack (fixed, do not substitute) - Runtime: Tauri 2 with React and TypeScript · a desktop application that reads and writes a folder you own - Storage: Markdown files with front matter, media beside them, written atomically · the files are the journal; anything else is a cache - Index: SQLite with FTS5, rebuildable from the folder in one command · search needs an index, but an index must never be the only copy - Time: Local wall time plus the zone identifier, both stored, never one alone · an entry written at nine in Tokyo is not the same entry at nine in London - Media: Copied into the folder, originals never moved or modified · importing a photograph should not be able to lose the photograph ### Data model (create this before Phase 1) - On disk: entries/2026/2026-09-11-0743-<id>.md with YAML front matter, and media/2026/09/<id>-<n>.<ext> - entries(id, path, written_at_local, written_at_utc, timezone, latitude, longitude, place_label, body_sha256, indexed_at) - media(id, entry_id, path, mime, bytes, sha256, taken_at_local, taken_at_zone_known, latitude, longitude, width, height) - tags(entry_id, tag) and links(from_entry, to_entry) · both derived from the file, both rebuildable - revisions(entry_id, ordinal, body_sha256, path, at) · previous versions kept as files under revisions/ - entries_fts(entry_id, body) · an FTS5 table whose tokenizer is a recorded decision, not a default nobody looked at - Every table above can be dropped and rebuilt from the folder, and the rebuild is a test rather than a claim. ### Phase 1 · Files first, index second Build: Write each entry as a Markdown file with front matter holding the identifier, the local time, the zone, tags and any coordinates. Write to a temporary file, flush, then rename over the target, and keep the previous version under revisions/. Build the index by scanning the folder, and provide a rebuild command that deletes the database and reconstructs it from the files alone. Trap: Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. Done when: Deleting the database and running the rebuild reproduces every entry, tag, link and media reference, asserted in the suite against a fixture folder. An entry file opened in a plain text editor is readable and its front matter is valid YAML. Killing the process during a save leaves either the previous version or the complete new one, never a truncated file, tested by injecting a crash. Editing a file outside the app and rescanning updates the index without losing the external edit. Do not build yet: No search, no media import, no maps, no export. ### Phase 2 · Search that works in the language you write in Build: Index entry bodies in FTS5 and choose the tokenizer deliberately. The default unicode61 tokenizer splits on characters that are not letters or numbers, which means a language written without spaces indexes as one enormous token and never matches. The trigram tokenizer gives substring matching and indexed LIKE and GLOB, but any query shorter than three characters matches no rows at all. Pick per the languages in the journal, record the choice in the README, and test both a two-character query and a query in a non-Latin script. Trap: Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. Done when: A search in each script the journal contains returns the right entries, asserted with fixtures in at least two scripts. A query of fewer than three characters behaves by a rule stated in the README and in the interface rather than silently returning nothing. Search over a thousand entries returns in under two hundred milliseconds on the development machine, measured. Reindexing after a tokenizer change is a single command and does not touch the entry files. Do not build yet: No media import, no location handling, no export or backup. ### Phase 3 · Photographs, timestamps and places Build: Copy imported media into the folder, never move or rewrite the original. Read the capture time and coordinates from the file's metadata, and record whether the capture time carried a zone, because the common field does not. Apply the image orientation flag when rendering rather than rotating the stored file. Store the entry's local time and its zone identifier together, and show both when they disagree with the machine's current zone. Trap: Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. Done when: An image whose capture time has no zone is recorded as zone-unknown and never silently assigned the machine's zone, tested with a fixture. A photograph with an orientation flag renders upright while the stored file is byte-identical to the original, verified by checksum. Coordinates from media are stored separately from coordinates the person set on the entry, and both are visible. An entry written in one zone and viewed in another shows the original local time with its zone named. Do not build yet: No revision browser, no export, no backup or sharing. ### Phase 4 · Revisions and a timeline worth trusting Build: Keep each previous body as a file under revisions/ with its hash, and offer a diff and a restore. Let an entry be backdated, storing the written time and the recorded time separately so a memory written today about last March appears in March without pretending it was written then. Build the calendar and the on-this-day view from the entry's own local date, not from a converted instant. Trap: Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. Done when: Restoring a previous revision writes a new file and keeps the revision that was replaced, so nothing is destroyed by a restore. A backdated entry appears on the backdated date and its record of when it was actually written is visible. Entries written at eleven at night in a distant zone group on their own local date, tested with fixtures from three zones. The revisions folder is human-readable and included in the rebuild. Do not build yet: No export, backup or sharing yet. ### Phase 5 · Leaving with everything, and leaking nothing Build: Export the folder as it stands plus a manifest, and prove the export by reimporting it into an empty install and comparing hashes. Strip location metadata from media on any share or export unless that action explicitly asked to keep it, and show which fields are travelling. Offer an encrypted backup to storage the person controls, with a restore exercised in the suite. Bind any local server the app runs to the loopback interface only. Trap: Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. Done when: An export reimported into an empty install reproduces every entry, revision and media file, compared by hash in the suite. A shared or exported image has no location metadata unless that action asked for it, asserted by reading the output file. A backup restore reproduces a working journal, exercised in the suite rather than documented. The share dialog lists exactly which fields leave with the entry. Do not build yet: Nothing after this. ### Out of scope (and why) - Conflict-safe multi-device sync, which is a distributed systems project in its own right and the thing a journal most often loses data to. - Native mobile clients, which double the storage and media work and need their own release process. - Collaborative editing and shared journals, which change the privacy model of every feature above. - Hosted storage you operate, which makes you the custodian of other people's diaries. ### README must contain - That entries are Markdown files with front matter and the database is a rebuildable index - The exact command that deletes the index and rebuilds it from the folder - Which FTS5 tokenizer was chosen, why, and what a query shorter than three characters does - That media is copied unmodified and orientation is applied at render time only - That location metadata is stripped from anything leaving unless that action asked to keep it ===== ARCHITECTURE.md ===== # Architecture · Journey ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript | a desktop application that reads and writes a folder you own | | Storage | Markdown files with front matter, media beside them, written atomically | the files are the journal; anything else is a cache | | Index | SQLite with FTS5, rebuildable from the folder in one command | search needs an index, but an index must never be the only copy | | Time | Local wall time plus the zone identifier, both stored, never one alone | an entry written at nine in Tokyo is not the same entry at nine in London | | Media | Copied into the folder, originals never moved or modified | importing a photograph should not be able to lose the photograph | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Storage | Markdown files, atomic writes and revisions. | Any file layout, provided the files remain the only copy that matters. | | Index | The rebuildable SQLite index and FTS5. | Any search engine, provided it can be deleted and rebuilt from the folder. | | Media | Unmodified imports, metadata reading and render-time orientation. | Any image library, provided the stored file stays byte-identical. | | Time | Local time with a zone, and zone-unknown capture times. | Not swappable for storing instants alone, which misfiles the timeline. | | Export | Round-tripped exports and location scrubbing. | Any format, provided the reimport comparison stays in the suite. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `JOURNAL_DIR` · required · The folder that is the journal. Everything else is rebuildable from it. - `INDEX_PATH` · required · The SQLite index. Safe to delete, because the rebuild is tested. - `FTS_TOKENIZER` · required · Recorded as a decision. Changing it reindexes and touches no entry file. - `REVISION_KEEP` · required · How many previous bodies to keep per entry, as files under revisions/. - `STRIP_LOCATION_ON_EXPORT` · required · The default for anything leaving the machine. Overridden per action, never globally by accident. - `BACKUP_TARGET` · optional · Encrypted backup destination, if you configure one. - `BACKUP_PASSPHRASE` · optional, secret · Encrypts the backup. Lose it and the backup is noise. ## 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 · Journey product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, Markdown files with front matter, media beside them, written atomically, SQLite with FTS5, rebuildable from the folder in one command, Local wall time plus the zone identifier, both stored, never one alone, Copied into the folder, originals never moved or modified. - 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 - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. - Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. ===== MILESTONES.md ===== # Delivery milestones · Journey Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files first, index second Entries live as Markdown on disk and the database can be deleted without losing anything. ### Steps 1. Write the file, then index it The order is the whole guarantee; reversing it makes the folder a copy. Files: `src-tauri/src/storage.rs`, `src/entry/save.ts` 2. Rename over the target after a flush A truncated entry file is the one loss this design exists to prevent. Files: `src-tauri/src/storage.rs`, `tests/atomic-write.test.ts` 3. Make the rebuild a tested command An index nobody has rebuilt is an index that cannot be rebuilt. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] A deleted database rebuilds from the folder alone - [ ] Entry files are readable in a plain text editor - [ ] A crash mid-save leaves no truncated file - [ ] An external edit survives a rescan ### Watch out - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. ## M2 · Search that works in the language you write in Full-text search returns the right entries for the scripts and short words the person actually uses. ### Steps 1. Choose the tokenizer against the real languages The default is right for English prose and wrong for anything written without spaces. Files: `src/index/fts.ts`, `docs/search.md` 2. Handle the short query case explicitly Silently returning nothing for a two-character search reads as a broken app. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Test search in two scripts One script in the fixtures means one script that works. Files: `tests/search-scripts.test.ts` ### Done when - [ ] Search works in at least two scripts, asserted - [ ] Short queries follow a stated rule, not silence - [ ] A thousand entries search in under 200 ms - [ ] Reindexing never touches the entry files ### Watch out - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. ## M3 · Photographs, timestamps and places Imported media keeps its own truth and the entry keeps a time that means something. ### Steps 1. Copy media in, never modify the original Verified by comparing checksums before and after import. Files: `src/media/import.ts`, `tests/media-untouched.test.ts` 2. Record whether the capture time carried a zone The usual metadata field does not, and guessing makes a holiday land on the wrong days. Files: `src/media/exif.ts` 3. Rotate at render, not on disk The orientation flag is information, not a defect to be corrected in the file. Files: `src/media/render.tsx` ### Done when - [ ] Zone-unknown capture times stay unknown - [ ] Imported media is byte-identical to the original - [ ] Media and manual coordinates are stored separately - [ ] An entry shows its original local time and zone ### Watch out - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. ## M4 · Revisions and a timeline worth trusting Edits are recoverable and the calendar view never misplaces an entry. ### Steps 1. Keep revisions as files with hashes A revision inside the database contradicts the entire storage rule. Files: `src/entry/revisions.ts` 2. Separate the written date from the recorded date Backdating is honest only when both are kept. Files: `src/entry/dates.ts` 3. Group the timeline by local date Converting to the current zone misfiles every entry written abroad at night. Files: `src/views/timeline.tsx`, `tests/timeline-zones.test.ts` ### Done when - [ ] A restore preserves the replaced revision - [ ] Backdated entries show both dates - [ ] Late-night entries abroad group on their local date - [ ] Revisions are readable and rebuilt into the index ### Watch out - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ## M5 · Leaving with everything, and leaking nothing (production only) Export, backup and sharing move the whole journal without carrying metadata nobody asked to send. ### Steps 1. Prove the export by reimporting it An export nobody has reimported is a folder of hope. Files: `src/export/run.ts`, `tests/export-roundtrip.test.ts` 2. Strip location on anything that leaves The preview does not show the coordinates, which is exactly why this leaks. Files: `src/share/scrub.ts`, `tests/no-gps-on-export.test.ts` 3. Exercise the backup restore in the suite Encrypted backups fail silently more often than they fail loudly. Files: `src/backup/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] An export reimports and matches by hash - [ ] Exported media carries no location unless asked - [ ] A backup restore is exercised in the suite - [ ] The share dialog names the travelling fields ### Watch out - Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. ===== OPERATIONS.md ===== # Operations · Journey ## Backup The journal folder is the backup unit, not the database. An encrypted copy offsite plus a local copy on different hardware, and the passphrase written down somewhere physical, because an encrypted backup with a forgotten passphrase is an elaborate way to delete a diary. ## Restore Restore the folder and run the index rebuild. That is the entire procedure, and it is the same command the suite runs, which is the point of the storage rule. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application the checks live in the suite and in a startup scan: entries whose file hash no longer matches the index, media referenced by an entry but missing from disk, and the age of the last successful backup. ## Incident checklist A missing entry is investigated in the folder first, then in revisions/, then in the backup. The database is never the place to look, because by design it knows nothing the folder does not. 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 - [ ] A deleted index is confirmed to rebuild from the folder alone - [ ] A crash mid-save is confirmed to leave no truncated file - [ ] Search is confirmed to work in every script in the fixtures - [ ] Imported media is confirmed byte-identical to the original - [ ] Exported media is confirmed to carry no location unless asked ## Launch constraint Do not market omitted Journey 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 that is the journal. Everything else is rebuildable from it. JOURNAL_DIR=~/Documents/Journal # Required. The SQLite index. Safe to delete, because the rebuild is tested. INDEX_PATH=~/.local/share/journal/index.db # Required. Recorded as a decision. Changing it reindexes and touches no entry file. FTS_TOKENIZER=unicode61 # Required. How many previous bodies to keep per entry, as files under revisions/. REVISION_KEEP=50 # Required. The default for anything leaving the machine. Overridden per action, never globally by accident. STRIP_LOCATION_ON_EXPORT=true # Optional. Encrypted backup destination, if you configure one. BACKUP_TARGET=s3://my-bucket/journal # Optional · secret. Encrypts the backup. Lose it and the backup is noise. BACKUP_PASSPHRASE=a long passphrase, redacted
# Journey · indie build A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. Estimated effort: **multi-day**. 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 | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript | a desktop application that reads and writes a folder you own | | Storage | Markdown files with front matter, media beside them, written atomically | the files are the journal; anything else is a cache | | Index | SQLite with FTS5, rebuildable from the folder in one command | search needs an index, but an index must never be the only copy | | Time | Local wall time plus the zone identifier, both stored, never one alone | an entry written at nine in Tokyo is not the same entry at nine in London | | Media | Copied into the folder, originals never moved or modified | importing a photograph should not be able to lose the photograph | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain for Tauri 2** · free - Why: Builds the desktop shell and the file handling that must not corrupt an entry. - Get it: rustup.rs, then the Tauri prerequisites for your platform. - Verify: cargo tauri info reports no missing dependencies - [ ] **SQLite built with FTS5** · free - Why: Search is the feature that makes a long journal usable, and FTS5 is not present in every build. - Get it: Most distributions include it; check rather than assume. - Verify: A create virtual table using fts5 statement succeeds - [ ] **Photographs with and without location metadata** · free - Why: The zone-unknown capture time, the orientation flag and the embedded coordinates are all only testable with real files. - Get it: A few pictures from a phone, plus one with location switched off and one rotated. - Verify: Each behaves as its phase requires - [ ] **Which languages the journal will contain** · free - Why: It decides the FTS5 tokenizer, and changing that later means reindexing after the person has already stopped trusting search. - Get it: Write down every script you expect to write in, including the occasional one. - Verify: The tokenizer choice in the README names them - [ ] **Storage you control for encrypted backups** (optional) · pennies per month, or free on a spare disk - Why: A local-first journal with no offsite copy is one disk failure from nothing. - Get it: Any object storage or removable disk with a key you hold. - Verify: A backup written and restored into an empty install ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. - high-fidelity color, format, and export handling - native mobile clients - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing If one of those is essential to you, that is the reason to keep paying for Journey, and the README should say so rather than pretend.
# Build brief · Journey 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-first journal whose entries stay readable without the app that wrote them to replace Journey. 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. ### Rule zero A journal is the one application where the data has to outlive the software, and most of them fail that test quietly. So the entries are plain files on disk in a folder the person can open in any text editor: one Markdown file per entry with its metadata in front matter, media beside it under names that mean something. The database is an index, nothing more. Deleting it and rebuilding it from the folder is a tested operation, not a theory, and it is the proof that the files really are the truth rather than a copy that drifts. The second rule is about what an entry knows. A journal entry can carry a location, a device name and the exact second it was written, and the photographs carry coordinates of their own in their metadata. That is fine while it stays on the machine. The moment anything leaves, through an export, a share or a backup uploaded somewhere, location metadata is stripped unless the person asked for it in that specific action, and the interface says which fields are travelling. ### Stack (fixed, do not substitute) - Runtime: Tauri 2 with React and TypeScript · a desktop application that reads and writes a folder you own - Storage: Markdown files with front matter, media beside them, written atomically · the files are the journal; anything else is a cache - Index: SQLite with FTS5, rebuildable from the folder in one command · search needs an index, but an index must never be the only copy - Time: Local wall time plus the zone identifier, both stored, never one alone · an entry written at nine in Tokyo is not the same entry at nine in London - Media: Copied into the folder, originals never moved or modified · importing a photograph should not be able to lose the photograph ### Data model (create this before Phase 1) - On disk: entries/2026/2026-09-11-0743-<id>.md with YAML front matter, and media/2026/09/<id>-<n>.<ext> - entries(id, path, written_at_local, written_at_utc, timezone, latitude, longitude, place_label, body_sha256, indexed_at) - media(id, entry_id, path, mime, bytes, sha256, taken_at_local, taken_at_zone_known, latitude, longitude, width, height) - tags(entry_id, tag) and links(from_entry, to_entry) · both derived from the file, both rebuildable - revisions(entry_id, ordinal, body_sha256, path, at) · previous versions kept as files under revisions/ - entries_fts(entry_id, body) · an FTS5 table whose tokenizer is a recorded decision, not a default nobody looked at - Every table above can be dropped and rebuilt from the folder, and the rebuild is a test rather than a claim. ### Phase 1 · Files first, index second Build: Write each entry as a Markdown file with front matter holding the identifier, the local time, the zone, tags and any coordinates. Write to a temporary file, flush, then rename over the target, and keep the previous version under revisions/. Build the index by scanning the folder, and provide a rebuild command that deletes the database and reconstructs it from the files alone. Trap: Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. Done when: Deleting the database and running the rebuild reproduces every entry, tag, link and media reference, asserted in the suite against a fixture folder. An entry file opened in a plain text editor is readable and its front matter is valid YAML. Killing the process during a save leaves either the previous version or the complete new one, never a truncated file, tested by injecting a crash. Editing a file outside the app and rescanning updates the index without losing the external edit. Do not build yet: No search, no media import, no maps, no export. ### Phase 2 · Search that works in the language you write in Build: Index entry bodies in FTS5 and choose the tokenizer deliberately. The default unicode61 tokenizer splits on characters that are not letters or numbers, which means a language written without spaces indexes as one enormous token and never matches. The trigram tokenizer gives substring matching and indexed LIKE and GLOB, but any query shorter than three characters matches no rows at all. Pick per the languages in the journal, record the choice in the README, and test both a two-character query and a query in a non-Latin script. Trap: Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. Done when: A search in each script the journal contains returns the right entries, asserted with fixtures in at least two scripts. A query of fewer than three characters behaves by a rule stated in the README and in the interface rather than silently returning nothing. Search over a thousand entries returns in under two hundred milliseconds on the development machine, measured. Reindexing after a tokenizer change is a single command and does not touch the entry files. Do not build yet: No media import, no location handling, no export or backup. ### Phase 3 · Photographs, timestamps and places Build: Copy imported media into the folder, never move or rewrite the original. Read the capture time and coordinates from the file's metadata, and record whether the capture time carried a zone, because the common field does not. Apply the image orientation flag when rendering rather than rotating the stored file. Store the entry's local time and its zone identifier together, and show both when they disagree with the machine's current zone. Trap: Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. Done when: An image whose capture time has no zone is recorded as zone-unknown and never silently assigned the machine's zone, tested with a fixture. A photograph with an orientation flag renders upright while the stored file is byte-identical to the original, verified by checksum. Coordinates from media are stored separately from coordinates the person set on the entry, and both are visible. An entry written in one zone and viewed in another shows the original local time with its zone named. Do not build yet: No revision browser, no export, no backup or sharing. ### Phase 4 · Revisions and a timeline worth trusting Build: Keep each previous body as a file under revisions/ with its hash, and offer a diff and a restore. Let an entry be backdated, storing the written time and the recorded time separately so a memory written today about last March appears in March without pretending it was written then. Build the calendar and the on-this-day view from the entry's own local date, not from a converted instant. Trap: Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. Done when: Restoring a previous revision writes a new file and keeps the revision that was replaced, so nothing is destroyed by a restore. A backdated entry appears on the backdated date and its record of when it was actually written is visible. Entries written at eleven at night in a distant zone group on their own local date, tested with fixtures from three zones. The revisions folder is human-readable and included in the rebuild. Do not build yet: No export, backup or sharing yet. ### Phase 5 · Leaving with everything, and leaking nothing Build: Export the folder as it stands plus a manifest, and prove the export by reimporting it into an empty install and comparing hashes. Strip location metadata from media on any share or export unless that action explicitly asked to keep it, and show which fields are travelling. Offer an encrypted backup to storage the person controls, with a restore exercised in the suite. Bind any local server the app runs to the loopback interface only. Trap: Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. Done when: An export reimported into an empty install reproduces every entry, revision and media file, compared by hash in the suite. A shared or exported image has no location metadata unless that action asked for it, asserted by reading the output file. A backup restore reproduces a working journal, exercised in the suite rather than documented. The share dialog lists exactly which fields leave with the entry. Do not build yet: Nothing after this. ### Out of scope (and why) - Conflict-safe multi-device sync, which is a distributed systems project in its own right and the thing a journal most often loses data to. - Native mobile clients, which double the storage and media work and need their own release process. - Collaborative editing and shared journals, which change the privacy model of every feature above. - Hosted storage you operate, which makes you the custodian of other people's diaries. ### README must contain - That entries are Markdown files with front matter and the database is a rebuildable index - The exact command that deletes the index and rebuilds it from the folder - Which FTS5 tokenizer was chosen, why, and what a query shorter than three characters does - That media is copied unmodified and orientation is applied at render time only - That location metadata is stripped from anything leaving unless that action asked to keep it
# Agent instructions · Journey indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, Markdown files with front matter, media beside them, written atomically, SQLite with FTS5, rebuildable from the folder in one command, Local wall time plus the zone identifier, both stored, never one alone, Copied into the folder, originals never moved or modified. 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 - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering.
# Build plan · Journey A local journal whose hard problems are making plain files the real storage while keeping search fast, handling photo timestamps and coordinates honestly, and getting everything out in a form that reimports without loss. 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 first, index second Entries live as Markdown on disk and the database can be deleted without losing anything. ### Steps 1. Write the file, then index it The order is the whole guarantee; reversing it makes the folder a copy. Files: `src-tauri/src/storage.rs`, `src/entry/save.ts` 2. Rename over the target after a flush A truncated entry file is the one loss this design exists to prevent. Files: `src-tauri/src/storage.rs`, `tests/atomic-write.test.ts` 3. Make the rebuild a tested command An index nobody has rebuilt is an index that cannot be rebuilt. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] A deleted database rebuilds from the folder alone - [ ] Entry files are readable in a plain text editor - [ ] A crash mid-save leaves no truncated file - [ ] An external edit survives a rescan ### Watch out - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. ## Phase 2 · Search that works in the language you write in Full-text search returns the right entries for the scripts and short words the person actually uses. ### Steps 1. Choose the tokenizer against the real languages The default is right for English prose and wrong for anything written without spaces. Files: `src/index/fts.ts`, `docs/search.md` 2. Handle the short query case explicitly Silently returning nothing for a two-character search reads as a broken app. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Test search in two scripts One script in the fixtures means one script that works. Files: `tests/search-scripts.test.ts` ### Done when - [ ] Search works in at least two scripts, asserted - [ ] Short queries follow a stated rule, not silence - [ ] A thousand entries search in under 200 ms - [ ] Reindexing never touches the entry files ### Watch out - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. ## Phase 3 · Photographs, timestamps and places Imported media keeps its own truth and the entry keeps a time that means something. ### Steps 1. Copy media in, never modify the original Verified by comparing checksums before and after import. Files: `src/media/import.ts`, `tests/media-untouched.test.ts` 2. Record whether the capture time carried a zone The usual metadata field does not, and guessing makes a holiday land on the wrong days. Files: `src/media/exif.ts` 3. Rotate at render, not on disk The orientation flag is information, not a defect to be corrected in the file. Files: `src/media/render.tsx` ### Done when - [ ] Zone-unknown capture times stay unknown - [ ] Imported media is byte-identical to the original - [ ] Media and manual coordinates are stored separately - [ ] An entry shows its original local time and zone ### Watch out - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. ## Phase 4 · Revisions and a timeline worth trusting Edits are recoverable and the calendar view never misplaces an entry. ### Steps 1. Keep revisions as files with hashes A revision inside the database contradicts the entire storage rule. Files: `src/entry/revisions.ts` 2. Separate the written date from the recorded date Backdating is honest only when both are kept. Files: `src/entry/dates.ts` 3. Group the timeline by local date Converting to the current zone misfiles every entry written abroad at night. Files: `src/views/timeline.tsx`, `tests/timeline-zones.test.ts` ### Done when - [ ] A restore preserves the replaced revision - [ ] Backdated entries show both dates - [ ] Late-night entries abroad group on their local date - [ ] Revisions are readable and rebuilt into the index ### Watch out - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ## Not in this build - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. ## After v1, if you want it - A read-only viewer that opens the folder with no database at all, which also proves the storage rule - Map clustering over entry coordinates, computed locally from the index - An importer for one other journal's export format, written against real files rather than its documentation
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The folder that is the journal. Everything else is rebuildable from it. JOURNAL_DIR=~/Documents/Journal # Required. The SQLite index. Safe to delete, because the rebuild is tested. INDEX_PATH=~/.local/share/journal/index.db # Required. Recorded as a decision. Changing it reindexes and touches no entry file. FTS_TOKENIZER=unicode61 # Required. How many previous bodies to keep per entry, as files under revisions/. REVISION_KEEP=50 # Required. The default for anything leaving the machine. Overridden per action, never globally by accident. STRIP_LOCATION_ON_EXPORT=true # Optional. Encrypted backup destination, if you configure one. BACKUP_TARGET=s3://my-bucket/journal # Optional · secret. Encrypts the backup. Lose it and the backup is noise. BACKUP_PASSPHRASE=a long passphrase, redacted
# Journey · product brief ## Problem The visible digital journal loop is buildable, but a credible replacement needs more than the first screen. Journey earns its keep through polish, sync, importers, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A journal you could still read in twenty years with a text editor, whose search works in the languages you write in, whose photographs keep their own timestamps, and which does not post your coordinates to whoever you shared an entry with. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - local filesystem access ## Explicit non-goals for v1 - Multi-device sync. - Native mobile clients. - Shared or collaborative journals. - Hosted storage operated by you. - high-fidelity color, format, and export handling - native mobile clients - conflict-safe multi-device sync - mature importers and exporters - collaborative editing and sharing ## Success criteria - A deleted index is confirmed to rebuild from the folder alone - A crash mid-save is confirmed to leave no truncated file - Search is confirmed to work in every script in the fixtures - Imported media is confirmed byte-identical to the original - Exported media is confirmed to carry no location unless asked
# Build brief · Journey 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-first journal whose entries stay readable without the app that wrote them to replace Journey. 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. ### Rule zero A journal is the one application where the data has to outlive the software, and most of them fail that test quietly. So the entries are plain files on disk in a folder the person can open in any text editor: one Markdown file per entry with its metadata in front matter, media beside it under names that mean something. The database is an index, nothing more. Deleting it and rebuilding it from the folder is a tested operation, not a theory, and it is the proof that the files really are the truth rather than a copy that drifts. The second rule is about what an entry knows. A journal entry can carry a location, a device name and the exact second it was written, and the photographs carry coordinates of their own in their metadata. That is fine while it stays on the machine. The moment anything leaves, through an export, a share or a backup uploaded somewhere, location metadata is stripped unless the person asked for it in that specific action, and the interface says which fields are travelling. ### Stack (fixed, do not substitute) - Runtime: Tauri 2 with React and TypeScript · a desktop application that reads and writes a folder you own - Storage: Markdown files with front matter, media beside them, written atomically · the files are the journal; anything else is a cache - Index: SQLite with FTS5, rebuildable from the folder in one command · search needs an index, but an index must never be the only copy - Time: Local wall time plus the zone identifier, both stored, never one alone · an entry written at nine in Tokyo is not the same entry at nine in London - Media: Copied into the folder, originals never moved or modified · importing a photograph should not be able to lose the photograph ### Data model (create this before Phase 1) - On disk: entries/2026/2026-09-11-0743-<id>.md with YAML front matter, and media/2026/09/<id>-<n>.<ext> - entries(id, path, written_at_local, written_at_utc, timezone, latitude, longitude, place_label, body_sha256, indexed_at) - media(id, entry_id, path, mime, bytes, sha256, taken_at_local, taken_at_zone_known, latitude, longitude, width, height) - tags(entry_id, tag) and links(from_entry, to_entry) · both derived from the file, both rebuildable - revisions(entry_id, ordinal, body_sha256, path, at) · previous versions kept as files under revisions/ - entries_fts(entry_id, body) · an FTS5 table whose tokenizer is a recorded decision, not a default nobody looked at - Every table above can be dropped and rebuilt from the folder, and the rebuild is a test rather than a claim. ### Phase 1 · Files first, index second Build: Write each entry as a Markdown file with front matter holding the identifier, the local time, the zone, tags and any coordinates. Write to a temporary file, flush, then rename over the target, and keep the previous version under revisions/. Build the index by scanning the folder, and provide a rebuild command that deletes the database and reconstructs it from the files alone. Trap: Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. Done when: Deleting the database and running the rebuild reproduces every entry, tag, link and media reference, asserted in the suite against a fixture folder. An entry file opened in a plain text editor is readable and its front matter is valid YAML. Killing the process during a save leaves either the previous version or the complete new one, never a truncated file, tested by injecting a crash. Editing a file outside the app and rescanning updates the index without losing the external edit. Do not build yet: No search, no media import, no maps, no export. ### Phase 2 · Search that works in the language you write in Build: Index entry bodies in FTS5 and choose the tokenizer deliberately. The default unicode61 tokenizer splits on characters that are not letters or numbers, which means a language written without spaces indexes as one enormous token and never matches. The trigram tokenizer gives substring matching and indexed LIKE and GLOB, but any query shorter than three characters matches no rows at all. Pick per the languages in the journal, record the choice in the README, and test both a two-character query and a query in a non-Latin script. Trap: Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. Done when: A search in each script the journal contains returns the right entries, asserted with fixtures in at least two scripts. A query of fewer than three characters behaves by a rule stated in the README and in the interface rather than silently returning nothing. Search over a thousand entries returns in under two hundred milliseconds on the development machine, measured. Reindexing after a tokenizer change is a single command and does not touch the entry files. Do not build yet: No media import, no location handling, no export or backup. ### Phase 3 · Photographs, timestamps and places Build: Copy imported media into the folder, never move or rewrite the original. Read the capture time and coordinates from the file's metadata, and record whether the capture time carried a zone, because the common field does not. Apply the image orientation flag when rendering rather than rotating the stored file. Store the entry's local time and its zone identifier together, and show both when they disagree with the machine's current zone. Trap: Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. Done when: An image whose capture time has no zone is recorded as zone-unknown and never silently assigned the machine's zone, tested with a fixture. A photograph with an orientation flag renders upright while the stored file is byte-identical to the original, verified by checksum. Coordinates from media are stored separately from coordinates the person set on the entry, and both are visible. An entry written in one zone and viewed in another shows the original local time with its zone named. Do not build yet: No revision browser, no export, no backup or sharing. ### Phase 4 · Revisions and a timeline worth trusting Build: Keep each previous body as a file under revisions/ with its hash, and offer a diff and a restore. Let an entry be backdated, storing the written time and the recorded time separately so a memory written today about last March appears in March without pretending it was written then. Build the calendar and the on-this-day view from the entry's own local date, not from a converted instant. Trap: Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. Done when: Restoring a previous revision writes a new file and keeps the revision that was replaced, so nothing is destroyed by a restore. A backdated entry appears on the backdated date and its record of when it was actually written is visible. Entries written at eleven at night in a distant zone group on their own local date, tested with fixtures from three zones. The revisions folder is human-readable and included in the rebuild. Do not build yet: No export, backup or sharing yet. ### Phase 5 · Leaving with everything, and leaking nothing Build: Export the folder as it stands plus a manifest, and prove the export by reimporting it into an empty install and comparing hashes. Strip location metadata from media on any share or export unless that action explicitly asked to keep it, and show which fields are travelling. Offer an encrypted backup to storage the person controls, with a restore exercised in the suite. Bind any local server the app runs to the loopback interface only. Trap: Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog. Done when: An export reimported into an empty install reproduces every entry, revision and media file, compared by hash in the suite. A shared or exported image has no location metadata unless that action asked for it, asserted by reading the output file. A backup restore reproduces a working journal, exercised in the suite rather than documented. The share dialog lists exactly which fields leave with the entry. Do not build yet: Nothing after this. ### Out of scope (and why) - Conflict-safe multi-device sync, which is a distributed systems project in its own right and the thing a journal most often loses data to. - Native mobile clients, which double the storage and media work and need their own release process. - Collaborative editing and shared journals, which change the privacy model of every feature above. - Hosted storage you operate, which makes you the custodian of other people's diaries. ### README must contain - That entries are Markdown files with front matter and the database is a rebuildable index - The exact command that deletes the index and rebuilds it from the folder - Which FTS5 tokenizer was chosen, why, and what a query shorter than three characters does - That media is copied unmodified and orientation is applied at render time only - That location metadata is stripped from anything leaving unless that action asked to keep it
# Architecture · Journey ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Tauri 2 with React and TypeScript | a desktop application that reads and writes a folder you own | | Storage | Markdown files with front matter, media beside them, written atomically | the files are the journal; anything else is a cache | | Index | SQLite with FTS5, rebuildable from the folder in one command | search needs an index, but an index must never be the only copy | | Time | Local wall time plus the zone identifier, both stored, never one alone | an entry written at nine in Tokyo is not the same entry at nine in London | | Media | Copied into the folder, originals never moved or modified | importing a photograph should not be able to lose the photograph | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Storage | Markdown files, atomic writes and revisions. | Any file layout, provided the files remain the only copy that matters. | | Index | The rebuildable SQLite index and FTS5. | Any search engine, provided it can be deleted and rebuilt from the folder. | | Media | Unmodified imports, metadata reading and render-time orientation. | Any image library, provided the stored file stays byte-identical. | | Time | Local time with a zone, and zone-unknown capture times. | Not swappable for storing instants alone, which misfiles the timeline. | | Export | Round-tripped exports and location scrubbing. | Any format, provided the reimport comparison stays in the suite. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `JOURNAL_DIR` · required · The folder that is the journal. Everything else is rebuildable from it. - `INDEX_PATH` · required · The SQLite index. Safe to delete, because the rebuild is tested. - `FTS_TOKENIZER` · required · Recorded as a decision. Changing it reindexes and touches no entry file. - `REVISION_KEEP` · required · How many previous bodies to keep per entry, as files under revisions/. - `STRIP_LOCATION_ON_EXPORT` · required · The default for anything leaving the machine. Overridden per action, never globally by accident. - `BACKUP_TARGET` · optional · Encrypted backup destination, if you configure one. - `BACKUP_PASSPHRASE` · optional, secret · Encrypts the backup. Lose it and the backup is noise. ## 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 · Journey product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, Markdown files with front matter, media beside them, written atomically, SQLite with FTS5, rebuildable from the folder in one command, Local wall time plus the zone identifier, both stored, never one alone, Copied into the folder, originals never moved or modified. - 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 - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. - Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog.
# Delivery milestones · Journey Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Files first, index second Entries live as Markdown on disk and the database can be deleted without losing anything. ### Steps 1. Write the file, then index it The order is the whole guarantee; reversing it makes the folder a copy. Files: `src-tauri/src/storage.rs`, `src/entry/save.ts` 2. Rename over the target after a flush A truncated entry file is the one loss this design exists to prevent. Files: `src-tauri/src/storage.rs`, `tests/atomic-write.test.ts` 3. Make the rebuild a tested command An index nobody has rebuilt is an index that cannot be rebuilt. Files: `src/index/rebuild.ts`, `tests/rebuild.test.ts` ### Done when - [ ] A deleted database rebuilds from the folder alone - [ ] Entry files are readable in a plain text editor - [ ] A crash mid-save leaves no truncated file - [ ] An external edit survives a rescan ### Watch out - Storing entries in the database and exporting files on demand looks identical for a year and then diverges the first time an export fails halfway or a schema migration goes wrong, and the folder people trusted turns out to have been a stale copy. Write the file first, index afterwards, and test the rebuild so the dependency only runs one way. ## M2 · Search that works in the language you write in Full-text search returns the right entries for the scripts and short words the person actually uses. ### Steps 1. Choose the tokenizer against the real languages The default is right for English prose and wrong for anything written without spaces. Files: `src/index/fts.ts`, `docs/search.md` 2. Handle the short query case explicitly Silently returning nothing for a two-character search reads as a broken app. Files: `src/search/query.ts`, `tests/short-query.test.ts` 3. Test search in two scripts One script in the fixtures means one script that works. Files: `tests/search-scripts.test.ts` ### Done when - [ ] Search works in at least two scripts, asserted - [ ] Short queries follow a stated rule, not silence - [ ] A thousand entries search in under 200 ms - [ ] Reindexing never touches the entry files ### Watch out - Accepting the default tokenizer and testing with English prose ships a journal where a Japanese or Chinese entry is unfindable, and switching later means reindexing everything after the person has already decided search does not work. Choose the tokenizer against the actual languages in the journal, and put a two-character search and a non-Latin search in the suite on day one. ## M3 · Photographs, timestamps and places Imported media keeps its own truth and the entry keeps a time that means something. ### Steps 1. Copy media in, never modify the original Verified by comparing checksums before and after import. Files: `src/media/import.ts`, `tests/media-untouched.test.ts` 2. Record whether the capture time carried a zone The usual metadata field does not, and guessing makes a holiday land on the wrong days. Files: `src/media/exif.ts` 3. Rotate at render, not on disk The orientation flag is information, not a defect to be corrected in the file. Files: `src/media/render.tsx` ### Done when - [ ] Zone-unknown capture times stay unknown - [ ] Imported media is byte-identical to the original - [ ] Media and manual coordinates are stored separately - [ ] An entry shows its original local time and zone ### Watch out - Treating a photograph's capture time as if it were in the machine's current zone puts every picture from a holiday on the wrong day for the rest of the journal's life, and displaying the image without honouring its orientation flag shows half a trip sideways. Record whether the time carried a zone, ask when it did not, and rotate at render time only. ## M4 · Revisions and a timeline worth trusting Edits are recoverable and the calendar view never misplaces an entry. ### Steps 1. Keep revisions as files with hashes A revision inside the database contradicts the entire storage rule. Files: `src/entry/revisions.ts` 2. Separate the written date from the recorded date Backdating is honest only when both are kept. Files: `src/entry/dates.ts` 3. Group the timeline by local date Converting to the current zone misfiles every entry written abroad at night. Files: `src/views/timeline.tsx`, `tests/timeline-zones.test.ts` ### Done when - [ ] A restore preserves the replaced revision - [ ] Backdated entries show both dates - [ ] Late-night entries abroad group on their local date - [ ] Revisions are readable and rebuilt into the index ### Watch out - Grouping the timeline by converting each entry's stored instant into the current machine zone moves entries written late at night abroad onto the previous or next day, so a month of travel is silently misfiled. Group by the local date recorded with the entry, and keep the instant only for ordering. ## M5 · Leaving with everything, and leaking nothing (production only) Export, backup and sharing move the whole journal without carrying metadata nobody asked to send. ### Steps 1. Prove the export by reimporting it An export nobody has reimported is a folder of hope. Files: `src/export/run.ts`, `tests/export-roundtrip.test.ts` 2. Strip location on anything that leaves The preview does not show the coordinates, which is exactly why this leaks. Files: `src/share/scrub.ts`, `tests/no-gps-on-export.test.ts` 3. Exercise the backup restore in the suite Encrypted backups fail silently more often than they fail loudly. Files: `src/backup/restore.ts`, `tests/restore.test.ts` ### Done when - [ ] An export reimports and matches by hash - [ ] Exported media carries no location unless asked - [ ] A backup restore is exercised in the suite - [ ] The share dialog names the travelling fields ### Watch out - Sharing a single entry by copying the image file sends the coordinates of the house it was taken in, embedded in metadata nobody sees in the preview, and the person finds out from whoever received it. Strip location from anything leaving by default, make keeping it a deliberate per-action choice, and list the travelling fields in the dialog.
# Operations · Journey ## Backup The journal folder is the backup unit, not the database. An encrypted copy offsite plus a local copy on different hardware, and the passphrase written down somewhere physical, because an encrypted backup with a forgotten passphrase is an elaborate way to delete a diary. ## Restore Restore the folder and run the index rebuild. That is the entire procedure, and it is the same command the suite runs, which is the point of the storage rule. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local application the checks live in the suite and in a startup scan: entries whose file hash no longer matches the index, media referenced by an entry but missing from disk, and the age of the last successful backup. ## Incident checklist A missing entry is investigated in the folder first, then in revisions/, then in the backup. The database is never the place to look, because by design it knows nothing the folder does not. 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 - [ ] A deleted index is confirmed to rebuild from the folder alone - [ ] A crash mid-save is confirmed to leave no truncated file - [ ] Search is confirmed to work in every script in the fixtures - [ ] Imported media is confirmed byte-identical to the original - [ ] Exported media is confirmed to carry no location unless asked ## Launch constraint Do not market omitted Journey 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 that is the journal. Everything else is rebuildable from it. JOURNAL_DIR=~/Documents/Journal # Required. The SQLite index. Safe to delete, because the rebuild is tested. INDEX_PATH=~/.local/share/journal/index.db # Required. Recorded as a decision. Changing it reindexes and touches no entry file. FTS_TOKENIZER=unicode61 # Required. How many previous bodies to keep per entry, as files under revisions/. REVISION_KEEP=50 # Required. The default for anything leaving the machine. Overridden per action, never globally by accident. STRIP_LOCATION_ON_EXPORT=true # Optional. Encrypted backup destination, if you configure one. BACKUP_TARGET=s3://my-bucket/journal # Optional · secret. Encrypts the backup. Lose it and the backup is noise. BACKUP_PASSPHRASE=a long passphrase, redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
Journey: People pay for a writing surface they trust for years, plus migration tools, sync, and tiny interaction details that disappear into habit.
xhigh-fidelity color, format, and export handling
xnative mobile clients
xconflict-safe multi-device sync
xmature importers and exporters
xcollaborative editing and sharing
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Journey pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Journey Cloud: 70 MB and 60 entries; 2 shared web links; 10 Odyssey AI questions/day; limited photos and no audio/video upload |
| premium | custom | — | One-time $17.99 on iOS; unlocks paid features on that platform only; still 70 MB/60 cloud entries, 2 links and 10 AI questions/day |
| membership monthly | $6.99/user | — | All platforms/web; 10 GB Journey Cloud; 5,000 entries; 50 web links with 30 entries each; 50 AI questions/day |
| membership annual | — | $4.17/user | Same Membership limits across all platforms |
| lifetime membership | custom | — | One-time $199; Membership access across supported platforms |
| signature | custom | — | 200 GB Journey Cloud; 50,000 entries; 1,000 Odyssey AI chats/day; 100 links with 100 entries each; 2,000 cloud prints/exports per month |
free tier70 MB Journey Cloud storage, 60 journal entries, 2 shared web links and 10 Odyssey AI questions/day; limited photos and no audio/video upload
billingMembership monthly ($6.99) + annual ($49.99) + $199 lifetime; Premium is a one-time platform-specific purchase; Signature separately billed
hidden costsPremium must be bought separately per platform and does not raise the 70 MB/60-entry cloud cap. App Store still exposes older/alternate $3.99 monthly and $29.99 annual SKUs, but current official copy specifies $6.99/$49.99. Signature is a separate charge with no public price.
verified 2026-08-13 · source ↗
Vibecode Journey
Kinda. The core of Journey is buildable in a weekend with the prompt on this page, but there are real gaps: high-fidelity color, format, and export handling, native mobile clients. Read the honest list above before committing.
How much does Journey cost?
Journey's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Journey?
Honestly: high-fidelity color, format, and export handling; native mobile clients; conflict-safe multi-device sync; mature importers and exporters; collaborative editing and sharing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Journey?
Yes: moodiary (Cross-platform journaling with media, a trail map and WebDAV backup; reminders are thinner.) The prompt is for when you want it exactly your way.