Vibecode Hevy Pro
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hevy Pro, log workouts and routines locally with neutral progress charts. The hard boundary is mobile apps, social features, watch integration, sync, and polished tracking, plus hardware data, content, coaching, and trust.
You are building a lean indie version of Hevy Pro. 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 ===== # Hevy Pro · indie build A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. 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 | TypeScript with Vite, built as an installable progressive web application | the gym has no signal and an app store review is not worth it for one user | | Storage | IndexedDB through a thin wrapper | logging a set must work offline and must never wait on a network | | Charts | Hand-drawn SVG, no charting library | four chart types with fixed requirements, and a library costs more bytes than it saves | | Timing | Absolute timestamps, never interval counters | background tabs and locked screens throttle timers, and a rest timer that lies is worse than none | | Export | JSON for completeness and CSV for spreadsheets | your training history should outlive this application | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build and the copy lint. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A phone you can install the application on** · free - Why: Every important behaviour here is a phone behaviour: locked screen timers, one-handed use, storage eviction. Testing only in a desktop browser will mislead you. - Get it: Serve the development build over your local network and open it on your phone, or use a tunnelling tool to get an HTTPS URL, which service workers require. - Verify: The application installs to the home screen and opens without browser chrome - [ ] **An HTTPS URL for development** · free with cloudflared - Why: Service workers and installation require a secure context, and your laptop's local address is not one on a phone. - Get it: A tunnelling tool such as cloudflared, which is free, or a self-signed certificate you trust on the device. - Verify: The phone loads the site over HTTPS and the service worker registers - [ ] **An exercise list you are allowed to ship** · free - Why: Seeding a few hundred movements saves an hour of typing, but a scraped list from a commercial app is someone else's data. - Get it: Write your own from the movements you actually do, or use an openly licensed list such as the one published by the wger project, and keep the licence note. - Verify: The source of every seeded exercise is documented - [ ] **Someone willing to read your interface strings** · free - Why: Phase 6 needs a second reader for tone. You cannot audit your own wording for pressure because you know what you meant. - Get it: Ask one person, give them the printed strings, and ask specifically whether anything reads as judgement. ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. - mobile apps, social features, watch integration, sync, and polished tracking - wearable hardware data - coach network - clinical validation - large content library and social network If one of those is essential to you, that is the reason to keep paying for Hevy Pro, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Hevy Pro 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 an offline-first workout logger with routines, rest timers and neutral progress charts to replace Hevy Pro. 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 This is a tracker, not a coach and not a health product. It records what you did and shows it back plainly. It must not diagnose, must not score your body, must not shame a missed session, and must not set or enforce restrictive targets. Phase 4 makes that a test rather than an intention, because a tracker that starts nagging is how people end up with a worse relationship to exercise than they had before. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, built as an installable progressive web application · the gym has no signal and an app store review is not worth it for one user - Storage: IndexedDB through a thin wrapper · logging a set must work offline and must never wait on a network - Charts: Hand-drawn SVG, no charting library · four chart types with fixed requirements, and a library costs more bytes than it saves - Timing: Absolute timestamps, never interval counters · background tabs and locked screens throttle timers, and a rest timer that lies is worse than none - Export: JSON for completeness and CSV for spreadsheets · your training history should outlive this application ### Data model (create this before Phase 1) - exercises(id, name, muscle_group, equipment, is_custom) - routines(id, name, notes, created_at) - routine_items(id, routine_id, exercise_id, position, target_sets, target_reps, rest_seconds) - sessions(id, routine_id, started_at, ended_at, notes) - sets(id, session_id, exercise_id, position, weight, reps, rpe, completed_at) - Weight is stored in one canonical unit with the display unit as a preference, so switching units never rewrites history. ### Phase 1 · Offline shell and the exercise library Build: A Vite project with a service worker that precaches the shell. IndexedDB with the schema above and a seeded exercise library. Prove offline behaviour by turning the network off rather than by trusting the manifest. Trap: Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. Done when: With the network disabled the application opens, lists exercises and records a set that survives a reload. Reinstalling after clearing site data restores nothing, which is the behaviour that makes the export prompt necessary and is worth seeing once deliberately. Do not build yet: No routines, no timer, no charts. ### Phase 2 · Logging a workout, and a timer that does not lie Build: A live session screen: pick an exercise, log weight and reps per set, mark sets done. A rest timer started from an absolute timestamp so elapsed time is computed on wake rather than counted while awake. Large tap targets and no gesture that loses a set. Trap: setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. Done when: Starting a ninety second rest timer, locking the phone for three minutes and unlocking shows the rest as complete rather than mid-count. Every set logged during a session is present after force-quitting the application mid-session. No control needed to log a set requires two hands or a precise tap. Do not build yet: No routines and no charts yet. ### Phase 3 · Routines and prefill Build: Create routines from exercises with target sets and reps. Starting a routine creates a session prefilled with the weights and reps from the last time you did each exercise, shown as suggestions you can accept or change rather than as targets you must hit. Trap: Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. Done when: Starting a routine prefills each exercise from its most recent logged session, and an exercise never done before prefills empty rather than zero. Changing a prefilled value does not alter the historical session it came from. The wording next to a prefill describes the past and does not instruct. Do not build yet: No charts yet. ### Phase 4 · Neutral progress, enforced by a test Build: Volume over time, estimated one-rep max per exercise, session frequency and a plain session history. No streaks, no grades, no body composition scoring, no calorie targets. Add a copy lint that fails the build if a banned word appears in the interface strings. Trap: Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. Done when: The charts render from logged data with correct axes and no interpolation across gaps. The copy lint fails when a banned word is deliberately added to a string, and passes on the shipped strings. No screen displays a grade, a streak or a body composition judgement. Do not build yet: No export or sync yet. ### Phase 5 · Export and import Build: Export everything as JSON that round-trips exactly, and as CSV per table for spreadsheets. Import the JSON back into an empty installation. Prompt for an export periodically, because browser storage is not a promise. Trap: A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. Done when: Exporting, clearing all site data, and importing the file reproduces every session, set and routine exactly, verified by comparing counts and a checksum of the normalised data. The CSV opens in a spreadsheet with readable exercise names and explicit units. Do not build yet: Sync and distribution are the product phase. ### Phase 6 · Sync, devices and a safety review Build: Optional sync through a small server or a file-sync folder, with last-write-wins per set row and a visible conflict list rather than silent resolution. Then read every string in the application specifically for tone, and have someone else read them too. Trap: Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. Done when: Two devices logging different sessions offline both end up with all sessions after sync, with no duplicates. A set deleted on one device stays deleted after sync rather than reappearing. A second person has read the interface strings and their notes are recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Wearable and watch integration. The heart rate and recovery data behind the paid tiers comes from hardware you are not building. - The social feed and following other lifters, which is the network effect and the reason the app feels alive. - Coaching, programme generation and anything that tells you what to do. That is a different product with a duty of care attached. - Any clinical or diagnostic claim. This records numbers you typed and nothing more. ### README must contain - That this is a log rather than a coach, and what it deliberately refuses to do - That browser storage can be evicted and export is the only durable copy - That estimated one-rep max is a formula, not a measurement - How the rest timer stays correct across a locked screen - The copy lint, the banned word list, and how to run it ===== AGENTS.md ===== # Agent instructions · Hevy Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, built as an installable progressive web application, IndexedDB through a thin wrapper, Hand-drawn SVG, no charting library, Absolute timestamps, never interval counters, JSON for completeness and CSV for spreadsheets. 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 - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ===== BUILD_PLAN.md ===== # Build plan · Hevy Pro A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Offline shell and the exercise library The application opens and works with the network switched off, and logging survives a reload. ### Steps 1. Set up the project and the service worker Vite with a plugin that generates the service worker, a web app manifest with icons, and a precache of the shell. Files: `vite.config.ts`, `manifest.webmanifest` ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` 2. Create the IndexedDB schema with versioned migrations One place that opens the database and applies upgrades by version. Getting this wrong later means users with a half-migrated store. Files: `src/db.ts` 3. Seed the exercise library and allow custom entries A few hundred common movements with muscle group and equipment, plus the ability to add your own. Ship the seed as a JSON file so it is reviewable. Files: `src/data/exercises.json` ### Done when - [ ] With networking disabled the application opens and lists exercises - [ ] A set logged offline survives a page reload - [ ] A custom exercise persists and appears in search - [ ] The install prompt appears and installing keeps everything working ### Watch out - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. ## Phase 2 · Logging a workout, and a timer that does not lie You can run a whole session one-handed, and the rest timer is right after the phone has been in your pocket. ### Steps 1. Build the session screen for one-handed use Large increment and decrement controls for weight and reps, a big done button, and no swipe-to-delete without an undo. Files: `src/session.ts` 2. Implement the timer from timestamps Store started_at. Compute remaining on each animation frame and on the visibilitychange event. Never hold the remaining value as the source of truth. Files: `src/timer.ts` 3. Write every set immediately Persist on completion of each set, not at the end of the session. A crashed browser must cost you one set at most. Files: `src/db.ts` ### Done when - [ ] A rest timer is correct after three minutes with the screen locked - [ ] Force-quitting mid-session loses no completed set - [ ] Every control is reachable one-handed - [ ] Deleting a set offers an undo ### Watch out - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. ## Phase 3 · Routines and prefill Starting a familiar session takes one tap and already knows what you did last time. ### Steps 1. Build routine creation and editing Reorderable exercises with target sets, reps and rest. Keep targets optional, because plenty of people train without them. Files: `src/routines.ts` 2. Prefill from the most recent session Query the latest set rows per exercise. Distinguish clearly between no history and a logged zero. Files: `src/prefill.ts` 3. Keep history immutable Editing today's session must never write into a past session's rows. Add a test that edits a prefilled value and asserts the source session is unchanged. Files: `src/prefill.test.ts` ### Done when - [ ] A routine prefills from the most recent session for each exercise - [ ] An exercise with no history prefills empty rather than zero - [ ] Editing a prefilled value leaves the source session unchanged - [ ] Prefill wording describes the past rather than instructing ### Watch out - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. ## Phase 4 · Neutral progress, enforced by a test Charts show what happened, and the application cannot start moralising without a test failing. ### Steps 1. Draw the charts in SVG Volume per week, estimated one-rep max per exercise over time, and sessions per week. Gaps stay gaps rather than being connected by a line that implies data. Files: `src/charts.ts` 2. Write the copy lint with a banned word list A build step that scans interface strings for shaming and diagnostic language, including streak, failed, cheat, guilty, earned, burn off and body fat percentage judgements. Fails the build, not a warning. Files: `scripts/copy-lint.ts`, `scripts/banned-words.json` 3. Label estimates as estimates An estimated one-rep max is a formula applied to a set, not a measurement. Say so where it appears, so nobody treats it as a tested number. Files: `src/charts.ts` ### Done when - [ ] Charts render correctly and leave gaps unconnected - [ ] The copy lint fails on a deliberately added banned word - [ ] The copy lint passes on the shipped strings - [ ] Estimated figures are labelled as estimates wherever they appear ### Watch out - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. ## Phase 5 · Export and import Your training history is a file you hold, not a row in someone's database. ### Steps 1. Export lossless JSON Every table, with a schema version so a future import knows what it is reading. Files: `src/export.ts` 2. Export readable CSV Join identifiers to names and include units in the header. Nobody wants an exercise id in a spreadsheet. Files: `src/export.ts` 3. Import and verify by round trip Import into an empty database and compare a normalised checksum with the source. Round-trip equality is the test worth having. Files: `src/import.ts`, `src/export.test.ts` ### Done when - [ ] Export, clear data, import reproduces everything exactly - [ ] A normalised checksum matches before and after the round trip - [ ] The CSV shows exercise names and explicit units - [ ] An export prompt appears periodically and can be dismissed ### Watch out - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ## Not in this build - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. ## After v1, if you want it - Plate maths for loading a barbell, which is small and genuinely useful - Per-exercise notes carried forward, such as a bar height or a grip - A printable session card for a phone-free workout ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The default display unit. Storage is always canonical, so this only affects presentation. VITE_WEIGHT_UNIT_DEFAULT=kg # Required. How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. VITE_EXPORT_REMINDER_DAYS=14 # Optional. Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. VITE_SYNC_ENDPOINT=https://sync.example.com # Optional · secret. Product phase only. Shared secret between your devices and your sync endpoint. SYNC_TOKEN=replace-with-a-random-token
You are building a lean indie version of Hevy Pro. 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 ===== # Hevy Pro · indie build A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. 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 | TypeScript with Vite, built as an installable progressive web application | the gym has no signal and an app store review is not worth it for one user | | Storage | IndexedDB through a thin wrapper | logging a set must work offline and must never wait on a network | | Charts | Hand-drawn SVG, no charting library | four chart types with fixed requirements, and a library costs more bytes than it saves | | Timing | Absolute timestamps, never interval counters | background tabs and locked screens throttle timers, and a rest timer that lies is worse than none | | Export | JSON for completeness and CSV for spreadsheets | your training history should outlive this application | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build and the copy lint. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A phone you can install the application on** · free - Why: Every important behaviour here is a phone behaviour: locked screen timers, one-handed use, storage eviction. Testing only in a desktop browser will mislead you. - Get it: Serve the development build over your local network and open it on your phone, or use a tunnelling tool to get an HTTPS URL, which service workers require. - Verify: The application installs to the home screen and opens without browser chrome - [ ] **An HTTPS URL for development** · free with cloudflared - Why: Service workers and installation require a secure context, and your laptop's local address is not one on a phone. - Get it: A tunnelling tool such as cloudflared, which is free, or a self-signed certificate you trust on the device. - Verify: The phone loads the site over HTTPS and the service worker registers - [ ] **An exercise list you are allowed to ship** · free - Why: Seeding a few hundred movements saves an hour of typing, but a scraped list from a commercial app is someone else's data. - Get it: Write your own from the movements you actually do, or use an openly licensed list such as the one published by the wger project, and keep the licence note. - Verify: The source of every seeded exercise is documented - [ ] **Someone willing to read your interface strings** · free - Why: Phase 6 needs a second reader for tone. You cannot audit your own wording for pressure because you know what you meant. - Get it: Ask one person, give them the printed strings, and ask specifically whether anything reads as judgement. ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. - mobile apps, social features, watch integration, sync, and polished tracking - wearable hardware data - coach network - clinical validation - large content library and social network If one of those is essential to you, that is the reason to keep paying for Hevy Pro, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Hevy Pro 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 an offline-first workout logger with routines, rest timers and neutral progress charts to replace Hevy Pro. 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 This is a tracker, not a coach and not a health product. It records what you did and shows it back plainly. It must not diagnose, must not score your body, must not shame a missed session, and must not set or enforce restrictive targets. Phase 4 makes that a test rather than an intention, because a tracker that starts nagging is how people end up with a worse relationship to exercise than they had before. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, built as an installable progressive web application · the gym has no signal and an app store review is not worth it for one user - Storage: IndexedDB through a thin wrapper · logging a set must work offline and must never wait on a network - Charts: Hand-drawn SVG, no charting library · four chart types with fixed requirements, and a library costs more bytes than it saves - Timing: Absolute timestamps, never interval counters · background tabs and locked screens throttle timers, and a rest timer that lies is worse than none - Export: JSON for completeness and CSV for spreadsheets · your training history should outlive this application ### Data model (create this before Phase 1) - exercises(id, name, muscle_group, equipment, is_custom) - routines(id, name, notes, created_at) - routine_items(id, routine_id, exercise_id, position, target_sets, target_reps, rest_seconds) - sessions(id, routine_id, started_at, ended_at, notes) - sets(id, session_id, exercise_id, position, weight, reps, rpe, completed_at) - Weight is stored in one canonical unit with the display unit as a preference, so switching units never rewrites history. ### Phase 1 · Offline shell and the exercise library Build: A Vite project with a service worker that precaches the shell. IndexedDB with the schema above and a seeded exercise library. Prove offline behaviour by turning the network off rather than by trusting the manifest. Trap: Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. Done when: With the network disabled the application opens, lists exercises and records a set that survives a reload. Reinstalling after clearing site data restores nothing, which is the behaviour that makes the export prompt necessary and is worth seeing once deliberately. Do not build yet: No routines, no timer, no charts. ### Phase 2 · Logging a workout, and a timer that does not lie Build: A live session screen: pick an exercise, log weight and reps per set, mark sets done. A rest timer started from an absolute timestamp so elapsed time is computed on wake rather than counted while awake. Large tap targets and no gesture that loses a set. Trap: setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. Done when: Starting a ninety second rest timer, locking the phone for three minutes and unlocking shows the rest as complete rather than mid-count. Every set logged during a session is present after force-quitting the application mid-session. No control needed to log a set requires two hands or a precise tap. Do not build yet: No routines and no charts yet. ### Phase 3 · Routines and prefill Build: Create routines from exercises with target sets and reps. Starting a routine creates a session prefilled with the weights and reps from the last time you did each exercise, shown as suggestions you can accept or change rather than as targets you must hit. Trap: Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. Done when: Starting a routine prefills each exercise from its most recent logged session, and an exercise never done before prefills empty rather than zero. Changing a prefilled value does not alter the historical session it came from. The wording next to a prefill describes the past and does not instruct. Do not build yet: No charts yet. ### Phase 4 · Neutral progress, enforced by a test Build: Volume over time, estimated one-rep max per exercise, session frequency and a plain session history. No streaks, no grades, no body composition scoring, no calorie targets. Add a copy lint that fails the build if a banned word appears in the interface strings. Trap: Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. Done when: The charts render from logged data with correct axes and no interpolation across gaps. The copy lint fails when a banned word is deliberately added to a string, and passes on the shipped strings. No screen displays a grade, a streak or a body composition judgement. Do not build yet: No export or sync yet. ### Phase 5 · Export and import Build: Export everything as JSON that round-trips exactly, and as CSV per table for spreadsheets. Import the JSON back into an empty installation. Prompt for an export periodically, because browser storage is not a promise. Trap: A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. Done when: Exporting, clearing all site data, and importing the file reproduces every session, set and routine exactly, verified by comparing counts and a checksum of the normalised data. The CSV opens in a spreadsheet with readable exercise names and explicit units. Do not build yet: Sync and distribution are the product phase. ### Phase 6 · Sync, devices and a safety review Build: Optional sync through a small server or a file-sync folder, with last-write-wins per set row and a visible conflict list rather than silent resolution. Then read every string in the application specifically for tone, and have someone else read them too. Trap: Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. Done when: Two devices logging different sessions offline both end up with all sessions after sync, with no duplicates. A set deleted on one device stays deleted after sync rather than reappearing. A second person has read the interface strings and their notes are recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Wearable and watch integration. The heart rate and recovery data behind the paid tiers comes from hardware you are not building. - The social feed and following other lifters, which is the network effect and the reason the app feels alive. - Coaching, programme generation and anything that tells you what to do. That is a different product with a duty of care attached. - Any clinical or diagnostic claim. This records numbers you typed and nothing more. ### README must contain - That this is a log rather than a coach, and what it deliberately refuses to do - That browser storage can be evicted and export is the only durable copy - That estimated one-rep max is a formula, not a measurement - How the rest timer stays correct across a locked screen - The copy lint, the banned word list, and how to run it ===== AGENTS.md ===== # Agent instructions · Hevy Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, built as an installable progressive web application, IndexedDB through a thin wrapper, Hand-drawn SVG, no charting library, Absolute timestamps, never interval counters, JSON for completeness and CSV for spreadsheets. 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 - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ===== BUILD_PLAN.md ===== # Build plan · Hevy Pro A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Offline shell and the exercise library The application opens and works with the network switched off, and logging survives a reload. ### Steps 1. Set up the project and the service worker Vite with a plugin that generates the service worker, a web app manifest with icons, and a precache of the shell. Files: `vite.config.ts`, `manifest.webmanifest` ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` 2. Create the IndexedDB schema with versioned migrations One place that opens the database and applies upgrades by version. Getting this wrong later means users with a half-migrated store. Files: `src/db.ts` 3. Seed the exercise library and allow custom entries A few hundred common movements with muscle group and equipment, plus the ability to add your own. Ship the seed as a JSON file so it is reviewable. Files: `src/data/exercises.json` ### Done when - [ ] With networking disabled the application opens and lists exercises - [ ] A set logged offline survives a page reload - [ ] A custom exercise persists and appears in search - [ ] The install prompt appears and installing keeps everything working ### Watch out - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. ## Phase 2 · Logging a workout, and a timer that does not lie You can run a whole session one-handed, and the rest timer is right after the phone has been in your pocket. ### Steps 1. Build the session screen for one-handed use Large increment and decrement controls for weight and reps, a big done button, and no swipe-to-delete without an undo. Files: `src/session.ts` 2. Implement the timer from timestamps Store started_at. Compute remaining on each animation frame and on the visibilitychange event. Never hold the remaining value as the source of truth. Files: `src/timer.ts` 3. Write every set immediately Persist on completion of each set, not at the end of the session. A crashed browser must cost you one set at most. Files: `src/db.ts` ### Done when - [ ] A rest timer is correct after three minutes with the screen locked - [ ] Force-quitting mid-session loses no completed set - [ ] Every control is reachable one-handed - [ ] Deleting a set offers an undo ### Watch out - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. ## Phase 3 · Routines and prefill Starting a familiar session takes one tap and already knows what you did last time. ### Steps 1. Build routine creation and editing Reorderable exercises with target sets, reps and rest. Keep targets optional, because plenty of people train without them. Files: `src/routines.ts` 2. Prefill from the most recent session Query the latest set rows per exercise. Distinguish clearly between no history and a logged zero. Files: `src/prefill.ts` 3. Keep history immutable Editing today's session must never write into a past session's rows. Add a test that edits a prefilled value and asserts the source session is unchanged. Files: `src/prefill.test.ts` ### Done when - [ ] A routine prefills from the most recent session for each exercise - [ ] An exercise with no history prefills empty rather than zero - [ ] Editing a prefilled value leaves the source session unchanged - [ ] Prefill wording describes the past rather than instructing ### Watch out - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. ## Phase 4 · Neutral progress, enforced by a test Charts show what happened, and the application cannot start moralising without a test failing. ### Steps 1. Draw the charts in SVG Volume per week, estimated one-rep max per exercise over time, and sessions per week. Gaps stay gaps rather than being connected by a line that implies data. Files: `src/charts.ts` 2. Write the copy lint with a banned word list A build step that scans interface strings for shaming and diagnostic language, including streak, failed, cheat, guilty, earned, burn off and body fat percentage judgements. Fails the build, not a warning. Files: `scripts/copy-lint.ts`, `scripts/banned-words.json` 3. Label estimates as estimates An estimated one-rep max is a formula applied to a set, not a measurement. Say so where it appears, so nobody treats it as a tested number. Files: `src/charts.ts` ### Done when - [ ] Charts render correctly and leave gaps unconnected - [ ] The copy lint fails on a deliberately added banned word - [ ] The copy lint passes on the shipped strings - [ ] Estimated figures are labelled as estimates wherever they appear ### Watch out - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. ## Phase 5 · Export and import Your training history is a file you hold, not a row in someone's database. ### Steps 1. Export lossless JSON Every table, with a schema version so a future import knows what it is reading. Files: `src/export.ts` 2. Export readable CSV Join identifiers to names and include units in the header. Nobody wants an exercise id in a spreadsheet. Files: `src/export.ts` 3. Import and verify by round trip Import into an empty database and compare a normalised checksum with the source. Round-trip equality is the test worth having. Files: `src/import.ts`, `src/export.test.ts` ### Done when - [ ] Export, clear data, import reproduces everything exactly - [ ] A normalised checksum matches before and after the round trip - [ ] The CSV shows exercise names and explicit units - [ ] An export prompt appears periodically and can be dismissed ### Watch out - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ## Not in this build - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. ## After v1, if you want it - Plate maths for loading a barbell, which is small and genuinely useful - Per-exercise notes carried forward, such as a bar height or a grip - A printable session card for a phone-free workout ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The default display unit. Storage is always canonical, so this only affects presentation. VITE_WEIGHT_UNIT_DEFAULT=kg # Required. How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. VITE_EXPORT_REMINDER_DAYS=14 # Optional. Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. VITE_SYNC_ENDPOINT=https://sync.example.com # Optional · secret. Product phase only. Shared secret between your devices and your sync endpoint. SYNC_TOKEN=replace-with-a-random-token
You are building a production product version of Hevy Pro. 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 ===== # Hevy Pro · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hevy Pro, log workouts and routines locally with neutral progress charts. The hard boundary is mobile apps, social features, watch integration, sync, and polished tracking, plus hardware data, content, coaching, and trust. ## Product outcome A training log that works in a basement with no signal, keeps its timers honest across a locked screen, syncs between your own devices without losing a deletion, and has been read by someone else specifically for tone. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - iOS or Android device - optional HealthKit or Health Connect permission - local storage ## Explicit non-goals for v1 - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. - mobile apps, social features, watch integration, sync, and polished tracking - wearable hardware data - coach network - clinical validation - large content library and social network ## Success criteria - The copy lint passes and is proved to fail on a planted word - The export and import round trip matches by checksum - The rest timer is verified across a locked screen on a real phone - Sync is verified to preserve a deletion rather than resurrecting it - A second person has reviewed the interface strings for tone ===== BRIEF.md ===== # Build brief · Hevy Pro 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 an offline-first workout logger with routines, rest timers and neutral progress charts to replace Hevy Pro. 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 This is a tracker, not a coach and not a health product. It records what you did and shows it back plainly. It must not diagnose, must not score your body, must not shame a missed session, and must not set or enforce restrictive targets. Phase 4 makes that a test rather than an intention, because a tracker that starts nagging is how people end up with a worse relationship to exercise than they had before. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, built as an installable progressive web application · the gym has no signal and an app store review is not worth it for one user - Storage: IndexedDB through a thin wrapper · logging a set must work offline and must never wait on a network - Charts: Hand-drawn SVG, no charting library · four chart types with fixed requirements, and a library costs more bytes than it saves - Timing: Absolute timestamps, never interval counters · background tabs and locked screens throttle timers, and a rest timer that lies is worse than none - Export: JSON for completeness and CSV for spreadsheets · your training history should outlive this application ### Data model (create this before Phase 1) - exercises(id, name, muscle_group, equipment, is_custom) - routines(id, name, notes, created_at) - routine_items(id, routine_id, exercise_id, position, target_sets, target_reps, rest_seconds) - sessions(id, routine_id, started_at, ended_at, notes) - sets(id, session_id, exercise_id, position, weight, reps, rpe, completed_at) - Weight is stored in one canonical unit with the display unit as a preference, so switching units never rewrites history. ### Phase 1 · Offline shell and the exercise library Build: A Vite project with a service worker that precaches the shell. IndexedDB with the schema above and a seeded exercise library. Prove offline behaviour by turning the network off rather than by trusting the manifest. Trap: Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. Done when: With the network disabled the application opens, lists exercises and records a set that survives a reload. Reinstalling after clearing site data restores nothing, which is the behaviour that makes the export prompt necessary and is worth seeing once deliberately. Do not build yet: No routines, no timer, no charts. ### Phase 2 · Logging a workout, and a timer that does not lie Build: A live session screen: pick an exercise, log weight and reps per set, mark sets done. A rest timer started from an absolute timestamp so elapsed time is computed on wake rather than counted while awake. Large tap targets and no gesture that loses a set. Trap: setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. Done when: Starting a ninety second rest timer, locking the phone for three minutes and unlocking shows the rest as complete rather than mid-count. Every set logged during a session is present after force-quitting the application mid-session. No control needed to log a set requires two hands or a precise tap. Do not build yet: No routines and no charts yet. ### Phase 3 · Routines and prefill Build: Create routines from exercises with target sets and reps. Starting a routine creates a session prefilled with the weights and reps from the last time you did each exercise, shown as suggestions you can accept or change rather than as targets you must hit. Trap: Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. Done when: Starting a routine prefills each exercise from its most recent logged session, and an exercise never done before prefills empty rather than zero. Changing a prefilled value does not alter the historical session it came from. The wording next to a prefill describes the past and does not instruct. Do not build yet: No charts yet. ### Phase 4 · Neutral progress, enforced by a test Build: Volume over time, estimated one-rep max per exercise, session frequency and a plain session history. No streaks, no grades, no body composition scoring, no calorie targets. Add a copy lint that fails the build if a banned word appears in the interface strings. Trap: Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. Done when: The charts render from logged data with correct axes and no interpolation across gaps. The copy lint fails when a banned word is deliberately added to a string, and passes on the shipped strings. No screen displays a grade, a streak or a body composition judgement. Do not build yet: No export or sync yet. ### Phase 5 · Export and import Build: Export everything as JSON that round-trips exactly, and as CSV per table for spreadsheets. Import the JSON back into an empty installation. Prompt for an export periodically, because browser storage is not a promise. Trap: A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. Done when: Exporting, clearing all site data, and importing the file reproduces every session, set and routine exactly, verified by comparing counts and a checksum of the normalised data. The CSV opens in a spreadsheet with readable exercise names and explicit units. Do not build yet: Sync and distribution are the product phase. ### Phase 6 · Sync, devices and a safety review Build: Optional sync through a small server or a file-sync folder, with last-write-wins per set row and a visible conflict list rather than silent resolution. Then read every string in the application specifically for tone, and have someone else read them too. Trap: Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. Done when: Two devices logging different sessions offline both end up with all sessions after sync, with no duplicates. A set deleted on one device stays deleted after sync rather than reappearing. A second person has read the interface strings and their notes are recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Wearable and watch integration. The heart rate and recovery data behind the paid tiers comes from hardware you are not building. - The social feed and following other lifters, which is the network effect and the reason the app feels alive. - Coaching, programme generation and anything that tells you what to do. That is a different product with a duty of care attached. - Any clinical or diagnostic claim. This records numbers you typed and nothing more. ### README must contain - That this is a log rather than a coach, and what it deliberately refuses to do - That browser storage can be evicted and export is the only durable copy - That estimated one-rep max is a formula, not a measurement - How the rest timer stays correct across a locked screen - The copy lint, the banned word list, and how to run it ===== ARCHITECTURE.md ===== # Architecture · Hevy Pro ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, built as an installable progressive web application | the gym has no signal and an app store review is not worth it for one user | | Storage | IndexedDB through a thin wrapper | logging a set must work offline and must never wait on a network | | Charts | Hand-drawn SVG, no charting library | four chart types with fixed requirements, and a library costs more bytes than it saves | | Timing | Absolute timestamps, never interval counters | background tabs and locked screens throttle timers, and a rest timer that lies is worse than none | | Export | JSON for completeness and CSV for spreadsheets | your training history should outlive this application | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Store | IndexedDB schema, migrations and every write. | SQLite compiled to WebAssembly is the upgrade path if queries get complex. The migration discipline matters more than the engine. | | Session | Live logging and immediate persistence. | This is the hot path and the one users feel. Optimise here before anywhere else. | | Timer | Absolute-time rest tracking and completion notification. | The timestamp approach is not negotiable. What can change is how completion is announced. | | Charts | Rendering history without interpretation. | A charting library would work, but the constraint that nothing is interpolated across gaps must survive the swap. | | Copy lint | Keeping the tone promise mechanical. | The word list grows; the build failure on violation should not become a warning. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_WEIGHT_UNIT_DEFAULT` · required · The default display unit. Storage is always canonical, so this only affects presentation. - `VITE_EXPORT_REMINDER_DAYS` · required · How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. - `VITE_SYNC_ENDPOINT` · optional · Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. - `SYNC_TOKEN` · optional, secret · Product phase only. Shared secret between your devices and your sync endpoint. ## 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 · Hevy Pro product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, built as an installable progressive web application, IndexedDB through a thin wrapper, Hand-drawn SVG, no charting library, Absolute timestamps, never interval counters, JSON for completeness and CSV for spreadsheets. - 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 - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. - Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. ===== MILESTONES.md ===== # Delivery milestones · Hevy Pro Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Offline shell and the exercise library The application opens and works with the network switched off, and logging survives a reload. ### Steps 1. Set up the project and the service worker Vite with a plugin that generates the service worker, a web app manifest with icons, and a precache of the shell. Files: `vite.config.ts`, `manifest.webmanifest` ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` 2. Create the IndexedDB schema with versioned migrations One place that opens the database and applies upgrades by version. Getting this wrong later means users with a half-migrated store. Files: `src/db.ts` 3. Seed the exercise library and allow custom entries A few hundred common movements with muscle group and equipment, plus the ability to add your own. Ship the seed as a JSON file so it is reviewable. Files: `src/data/exercises.json` ### Done when - [ ] With networking disabled the application opens and lists exercises - [ ] A set logged offline survives a page reload - [ ] A custom exercise persists and appears in search - [ ] The install prompt appears and installing keeps everything working ### Watch out - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. ## M2 · Logging a workout, and a timer that does not lie You can run a whole session one-handed, and the rest timer is right after the phone has been in your pocket. ### Steps 1. Build the session screen for one-handed use Large increment and decrement controls for weight and reps, a big done button, and no swipe-to-delete without an undo. Files: `src/session.ts` 2. Implement the timer from timestamps Store started_at. Compute remaining on each animation frame and on the visibilitychange event. Never hold the remaining value as the source of truth. Files: `src/timer.ts` 3. Write every set immediately Persist on completion of each set, not at the end of the session. A crashed browser must cost you one set at most. Files: `src/db.ts` ### Done when - [ ] A rest timer is correct after three minutes with the screen locked - [ ] Force-quitting mid-session loses no completed set - [ ] Every control is reachable one-handed - [ ] Deleting a set offers an undo ### Watch out - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. ## M3 · Routines and prefill Starting a familiar session takes one tap and already knows what you did last time. ### Steps 1. Build routine creation and editing Reorderable exercises with target sets, reps and rest. Keep targets optional, because plenty of people train without them. Files: `src/routines.ts` 2. Prefill from the most recent session Query the latest set rows per exercise. Distinguish clearly between no history and a logged zero. Files: `src/prefill.ts` 3. Keep history immutable Editing today's session must never write into a past session's rows. Add a test that edits a prefilled value and asserts the source session is unchanged. Files: `src/prefill.test.ts` ### Done when - [ ] A routine prefills from the most recent session for each exercise - [ ] An exercise with no history prefills empty rather than zero - [ ] Editing a prefilled value leaves the source session unchanged - [ ] Prefill wording describes the past rather than instructing ### Watch out - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. ## M4 · Neutral progress, enforced by a test Charts show what happened, and the application cannot start moralising without a test failing. ### Steps 1. Draw the charts in SVG Volume per week, estimated one-rep max per exercise over time, and sessions per week. Gaps stay gaps rather than being connected by a line that implies data. Files: `src/charts.ts` 2. Write the copy lint with a banned word list A build step that scans interface strings for shaming and diagnostic language, including streak, failed, cheat, guilty, earned, burn off and body fat percentage judgements. Fails the build, not a warning. Files: `scripts/copy-lint.ts`, `scripts/banned-words.json` 3. Label estimates as estimates An estimated one-rep max is a formula applied to a set, not a measurement. Say so where it appears, so nobody treats it as a tested number. Files: `src/charts.ts` ### Done when - [ ] Charts render correctly and leave gaps unconnected - [ ] The copy lint fails on a deliberately added banned word - [ ] The copy lint passes on the shipped strings - [ ] Estimated figures are labelled as estimates wherever they appear ### Watch out - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. ## M5 · Export and import Your training history is a file you hold, not a row in someone's database. ### Steps 1. Export lossless JSON Every table, with a schema version so a future import knows what it is reading. Files: `src/export.ts` 2. Export readable CSV Join identifiers to names and include units in the header. Nobody wants an exercise id in a spreadsheet. Files: `src/export.ts` 3. Import and verify by round trip Import into an empty database and compare a normalised checksum with the source. Round-trip equality is the test worth having. Files: `src/import.ts`, `src/export.test.ts` ### Done when - [ ] Export, clear data, import reproduces everything exactly - [ ] A normalised checksum matches before and after the round trip - [ ] The CSV shows exercise names and explicit units - [ ] An export prompt appears periodically and can be dismissed ### Watch out - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ## M6 · Sync, devices and a safety review (production only) It works on more than one device, and someone other than you has read the words it says. ### Steps 1. Add sync with tombstones Every row carries updated_at and a deleted flag. Sync exchanges rows since a watermark. Deletions travel as tombstones, never as absences. Files: `src/sync.ts` 2. Show conflicts rather than hiding them When both sides changed the same row, keep both and show a short list. Training data is small and a person can resolve four conflicts. Files: `src/conflicts.ts` 3. Run a tone review with someone else Print every interface string and read it as though you had missed three weeks. Record what you changed. This is the phase where the rule zero promise is kept or broken. Files: `docs/tone-review.md` ### Done when - [ ] Two devices with offline sessions converge with no duplicates - [ ] A deletion survives sync rather than reappearing - [ ] Conflicts are visible and resolvable - [ ] A second reader's tone notes are recorded and acted on ### Watch out - Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. ===== OPERATIONS.md ===== # Operations · Hevy Pro ## Backup The user's export file is the backup. Prompt for it on a schedule and after any large session. If you add sync, the server is a convenience and not a backup, because a bug that deletes locally will happily sync the deletion. ## Restore Import the JSON export into an empty installation. The round-trip test from Phase 5 is what makes this trustworthy, so keep it in continuous integration. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring If there is no sync server there is nothing to monitor. With one, watch for rows rejected by the watermark logic and for conflict counts, because a rising conflict rate usually means clock skew rather than genuine concurrent editing. ## Incident checklist Storage eviction is the incident that actually happens. The response is the import path, which is why it must be tested rather than assumed. For a bad migration, refuse to open the database and offer an export of the raw store rather than upgrading into a broken state. 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 - [ ] The copy lint passes and is proved to fail on a planted word - [ ] The export and import round trip matches by checksum - [ ] The rest timer is verified across a locked screen on a real phone - [ ] Sync is verified to preserve a deletion rather than resurrecting it - [ ] A second person has reviewed the interface strings for tone ## Launch constraint Do not market omitted Hevy Pro 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 default display unit. Storage is always canonical, so this only affects presentation. VITE_WEIGHT_UNIT_DEFAULT=kg # Required. How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. VITE_EXPORT_REMINDER_DAYS=14 # Optional. Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. VITE_SYNC_ENDPOINT=https://sync.example.com # Optional · secret. Product phase only. Shared secret between your devices and your sync endpoint. SYNC_TOKEN=replace-with-a-random-token
# Hevy Pro · indie build A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. 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 | TypeScript with Vite, built as an installable progressive web application | the gym has no signal and an app store review is not worth it for one user | | Storage | IndexedDB through a thin wrapper | logging a set must work offline and must never wait on a network | | Charts | Hand-drawn SVG, no charting library | four chart types with fixed requirements, and a library costs more bytes than it saves | | Timing | Absolute timestamps, never interval counters | background tabs and locked screens throttle timers, and a rest timer that lies is worse than none | | Export | JSON for completeness and CSV for spreadsheets | your training history should outlive this application | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build and the copy lint. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A phone you can install the application on** · free - Why: Every important behaviour here is a phone behaviour: locked screen timers, one-handed use, storage eviction. Testing only in a desktop browser will mislead you. - Get it: Serve the development build over your local network and open it on your phone, or use a tunnelling tool to get an HTTPS URL, which service workers require. - Verify: The application installs to the home screen and opens without browser chrome - [ ] **An HTTPS URL for development** · free with cloudflared - Why: Service workers and installation require a secure context, and your laptop's local address is not one on a phone. - Get it: A tunnelling tool such as cloudflared, which is free, or a self-signed certificate you trust on the device. - Verify: The phone loads the site over HTTPS and the service worker registers - [ ] **An exercise list you are allowed to ship** · free - Why: Seeding a few hundred movements saves an hour of typing, but a scraped list from a commercial app is someone else's data. - Get it: Write your own from the movements you actually do, or use an openly licensed list such as the one published by the wger project, and keep the licence note. - Verify: The source of every seeded exercise is documented - [ ] **Someone willing to read your interface strings** · free - Why: Phase 6 needs a second reader for tone. You cannot audit your own wording for pressure because you know what you meant. - Get it: Ask one person, give them the printed strings, and ask specifically whether anything reads as judgement. ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. - mobile apps, social features, watch integration, sync, and polished tracking - wearable hardware data - coach network - clinical validation - large content library and social network If one of those is essential to you, that is the reason to keep paying for Hevy Pro, and the README should say so rather than pretend.
# Build brief · Hevy Pro 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 an offline-first workout logger with routines, rest timers and neutral progress charts to replace Hevy Pro. 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 This is a tracker, not a coach and not a health product. It records what you did and shows it back plainly. It must not diagnose, must not score your body, must not shame a missed session, and must not set or enforce restrictive targets. Phase 4 makes that a test rather than an intention, because a tracker that starts nagging is how people end up with a worse relationship to exercise than they had before. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, built as an installable progressive web application · the gym has no signal and an app store review is not worth it for one user - Storage: IndexedDB through a thin wrapper · logging a set must work offline and must never wait on a network - Charts: Hand-drawn SVG, no charting library · four chart types with fixed requirements, and a library costs more bytes than it saves - Timing: Absolute timestamps, never interval counters · background tabs and locked screens throttle timers, and a rest timer that lies is worse than none - Export: JSON for completeness and CSV for spreadsheets · your training history should outlive this application ### Data model (create this before Phase 1) - exercises(id, name, muscle_group, equipment, is_custom) - routines(id, name, notes, created_at) - routine_items(id, routine_id, exercise_id, position, target_sets, target_reps, rest_seconds) - sessions(id, routine_id, started_at, ended_at, notes) - sets(id, session_id, exercise_id, position, weight, reps, rpe, completed_at) - Weight is stored in one canonical unit with the display unit as a preference, so switching units never rewrites history. ### Phase 1 · Offline shell and the exercise library Build: A Vite project with a service worker that precaches the shell. IndexedDB with the schema above and a seeded exercise library. Prove offline behaviour by turning the network off rather than by trusting the manifest. Trap: Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. Done when: With the network disabled the application opens, lists exercises and records a set that survives a reload. Reinstalling after clearing site data restores nothing, which is the behaviour that makes the export prompt necessary and is worth seeing once deliberately. Do not build yet: No routines, no timer, no charts. ### Phase 2 · Logging a workout, and a timer that does not lie Build: A live session screen: pick an exercise, log weight and reps per set, mark sets done. A rest timer started from an absolute timestamp so elapsed time is computed on wake rather than counted while awake. Large tap targets and no gesture that loses a set. Trap: setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. Done when: Starting a ninety second rest timer, locking the phone for three minutes and unlocking shows the rest as complete rather than mid-count. Every set logged during a session is present after force-quitting the application mid-session. No control needed to log a set requires two hands or a precise tap. Do not build yet: No routines and no charts yet. ### Phase 3 · Routines and prefill Build: Create routines from exercises with target sets and reps. Starting a routine creates a session prefilled with the weights and reps from the last time you did each exercise, shown as suggestions you can accept or change rather than as targets you must hit. Trap: Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. Done when: Starting a routine prefills each exercise from its most recent logged session, and an exercise never done before prefills empty rather than zero. Changing a prefilled value does not alter the historical session it came from. The wording next to a prefill describes the past and does not instruct. Do not build yet: No charts yet. ### Phase 4 · Neutral progress, enforced by a test Build: Volume over time, estimated one-rep max per exercise, session frequency and a plain session history. No streaks, no grades, no body composition scoring, no calorie targets. Add a copy lint that fails the build if a banned word appears in the interface strings. Trap: Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. Done when: The charts render from logged data with correct axes and no interpolation across gaps. The copy lint fails when a banned word is deliberately added to a string, and passes on the shipped strings. No screen displays a grade, a streak or a body composition judgement. Do not build yet: No export or sync yet. ### Phase 5 · Export and import Build: Export everything as JSON that round-trips exactly, and as CSV per table for spreadsheets. Import the JSON back into an empty installation. Prompt for an export periodically, because browser storage is not a promise. Trap: A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. Done when: Exporting, clearing all site data, and importing the file reproduces every session, set and routine exactly, verified by comparing counts and a checksum of the normalised data. The CSV opens in a spreadsheet with readable exercise names and explicit units. Do not build yet: Sync and distribution are the product phase. ### Phase 6 · Sync, devices and a safety review Build: Optional sync through a small server or a file-sync folder, with last-write-wins per set row and a visible conflict list rather than silent resolution. Then read every string in the application specifically for tone, and have someone else read them too. Trap: Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. Done when: Two devices logging different sessions offline both end up with all sessions after sync, with no duplicates. A set deleted on one device stays deleted after sync rather than reappearing. A second person has read the interface strings and their notes are recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Wearable and watch integration. The heart rate and recovery data behind the paid tiers comes from hardware you are not building. - The social feed and following other lifters, which is the network effect and the reason the app feels alive. - Coaching, programme generation and anything that tells you what to do. That is a different product with a duty of care attached. - Any clinical or diagnostic claim. This records numbers you typed and nothing more. ### README must contain - That this is a log rather than a coach, and what it deliberately refuses to do - That browser storage can be evicted and export is the only durable copy - That estimated one-rep max is a formula, not a measurement - How the rest timer stays correct across a locked screen - The copy lint, the banned word list, and how to run it
# Agent instructions · Hevy Pro indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, built as an installable progressive web application, IndexedDB through a thin wrapper, Hand-drawn SVG, no charting library, Absolute timestamps, never interval counters, JSON for completeness and CSV for spreadsheets. 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 - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface.
# Build plan · Hevy Pro A progressive web application that works with no signal in a gym basement: an exercise library, routines, live set logging with a rest timer that survives a locked screen, previous-session prefill, neutral progress charts, and export of everything you have logged as JSON and CSV. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Offline shell and the exercise library The application opens and works with the network switched off, and logging survives a reload. ### Steps 1. Set up the project and the service worker Vite with a plugin that generates the service worker, a web app manifest with icons, and a precache of the shell. Files: `vite.config.ts`, `manifest.webmanifest` ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` 2. Create the IndexedDB schema with versioned migrations One place that opens the database and applies upgrades by version. Getting this wrong later means users with a half-migrated store. Files: `src/db.ts` 3. Seed the exercise library and allow custom entries A few hundred common movements with muscle group and equipment, plus the ability to add your own. Ship the seed as a JSON file so it is reviewable. Files: `src/data/exercises.json` ### Done when - [ ] With networking disabled the application opens and lists exercises - [ ] A set logged offline survives a page reload - [ ] A custom exercise persists and appears in search - [ ] The install prompt appears and installing keeps everything working ### Watch out - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. ## Phase 2 · Logging a workout, and a timer that does not lie You can run a whole session one-handed, and the rest timer is right after the phone has been in your pocket. ### Steps 1. Build the session screen for one-handed use Large increment and decrement controls for weight and reps, a big done button, and no swipe-to-delete without an undo. Files: `src/session.ts` 2. Implement the timer from timestamps Store started_at. Compute remaining on each animation frame and on the visibilitychange event. Never hold the remaining value as the source of truth. Files: `src/timer.ts` 3. Write every set immediately Persist on completion of each set, not at the end of the session. A crashed browser must cost you one set at most. Files: `src/db.ts` ### Done when - [ ] A rest timer is correct after three minutes with the screen locked - [ ] Force-quitting mid-session loses no completed set - [ ] Every control is reachable one-handed - [ ] Deleting a set offers an undo ### Watch out - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. ## Phase 3 · Routines and prefill Starting a familiar session takes one tap and already knows what you did last time. ### Steps 1. Build routine creation and editing Reorderable exercises with target sets, reps and rest. Keep targets optional, because plenty of people train without them. Files: `src/routines.ts` 2. Prefill from the most recent session Query the latest set rows per exercise. Distinguish clearly between no history and a logged zero. Files: `src/prefill.ts` 3. Keep history immutable Editing today's session must never write into a past session's rows. Add a test that edits a prefilled value and asserts the source session is unchanged. Files: `src/prefill.test.ts` ### Done when - [ ] A routine prefills from the most recent session for each exercise - [ ] An exercise with no history prefills empty rather than zero - [ ] Editing a prefilled value leaves the source session unchanged - [ ] Prefill wording describes the past rather than instructing ### Watch out - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. ## Phase 4 · Neutral progress, enforced by a test Charts show what happened, and the application cannot start moralising without a test failing. ### Steps 1. Draw the charts in SVG Volume per week, estimated one-rep max per exercise over time, and sessions per week. Gaps stay gaps rather than being connected by a line that implies data. Files: `src/charts.ts` 2. Write the copy lint with a banned word list A build step that scans interface strings for shaming and diagnostic language, including streak, failed, cheat, guilty, earned, burn off and body fat percentage judgements. Fails the build, not a warning. Files: `scripts/copy-lint.ts`, `scripts/banned-words.json` 3. Label estimates as estimates An estimated one-rep max is a formula applied to a set, not a measurement. Say so where it appears, so nobody treats it as a tested number. Files: `src/charts.ts` ### Done when - [ ] Charts render correctly and leave gaps unconnected - [ ] The copy lint fails on a deliberately added banned word - [ ] The copy lint passes on the shipped strings - [ ] Estimated figures are labelled as estimates wherever they appear ### Watch out - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. ## Phase 5 · Export and import Your training history is a file you hold, not a row in someone's database. ### Steps 1. Export lossless JSON Every table, with a schema version so a future import knows what it is reading. Files: `src/export.ts` 2. Export readable CSV Join identifiers to names and include units in the header. Nobody wants an exercise id in a spreadsheet. Files: `src/export.ts` 3. Import and verify by round trip Import into an empty database and compare a normalised checksum with the source. Round-trip equality is the test worth having. Files: `src/import.ts`, `src/export.test.ts` ### Done when - [ ] Export, clear data, import reproduces everything exactly - [ ] A normalised checksum matches before and after the round trip - [ ] The CSV shows exercise names and explicit units - [ ] An export prompt appears periodically and can be dismissed ### Watch out - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ## Not in this build - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. ## After v1, if you want it - Plate maths for loading a barbell, which is small and genuinely useful - Per-exercise notes carried forward, such as a bar height or a grip - A printable session card for a phone-free workout
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The default display unit. Storage is always canonical, so this only affects presentation. VITE_WEIGHT_UNIT_DEFAULT=kg # Required. How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. VITE_EXPORT_REMINDER_DAYS=14 # Optional. Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. VITE_SYNC_ENDPOINT=https://sync.example.com # Optional · secret. Product phase only. Shared secret between your devices and your sync endpoint. SYNC_TOKEN=replace-with-a-random-token
# Hevy Pro · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For Hevy Pro, log workouts and routines locally with neutral progress charts. The hard boundary is mobile apps, social features, watch integration, sync, and polished tracking, plus hardware data, content, coaching, and trust. ## Product outcome A training log that works in a basement with no signal, keeps its timers honest across a locked screen, syncs between your own devices without losing a deletion, and has been read by someone else specifically for tone. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - iOS or Android device - optional HealthKit or Health Connect permission - local storage ## Explicit non-goals for v1 - Wearable and watch data, which needs hardware and platform health permissions. - The social feed, following and the community that makes the paid app feel populated. - Programme generation and coaching of any kind. - Any diagnostic, clinical or body composition claim. - mobile apps, social features, watch integration, sync, and polished tracking - wearable hardware data - coach network - clinical validation - large content library and social network ## Success criteria - The copy lint passes and is proved to fail on a planted word - The export and import round trip matches by checksum - The rest timer is verified across a locked screen on a real phone - Sync is verified to preserve a deletion rather than resurrecting it - A second person has reviewed the interface strings for tone
# Build brief · Hevy Pro 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 an offline-first workout logger with routines, rest timers and neutral progress charts to replace Hevy Pro. 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 This is a tracker, not a coach and not a health product. It records what you did and shows it back plainly. It must not diagnose, must not score your body, must not shame a missed session, and must not set or enforce restrictive targets. Phase 4 makes that a test rather than an intention, because a tracker that starts nagging is how people end up with a worse relationship to exercise than they had before. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, built as an installable progressive web application · the gym has no signal and an app store review is not worth it for one user - Storage: IndexedDB through a thin wrapper · logging a set must work offline and must never wait on a network - Charts: Hand-drawn SVG, no charting library · four chart types with fixed requirements, and a library costs more bytes than it saves - Timing: Absolute timestamps, never interval counters · background tabs and locked screens throttle timers, and a rest timer that lies is worse than none - Export: JSON for completeness and CSV for spreadsheets · your training history should outlive this application ### Data model (create this before Phase 1) - exercises(id, name, muscle_group, equipment, is_custom) - routines(id, name, notes, created_at) - routine_items(id, routine_id, exercise_id, position, target_sets, target_reps, rest_seconds) - sessions(id, routine_id, started_at, ended_at, notes) - sets(id, session_id, exercise_id, position, weight, reps, rpe, completed_at) - Weight is stored in one canonical unit with the display unit as a preference, so switching units never rewrites history. ### Phase 1 · Offline shell and the exercise library Build: A Vite project with a service worker that precaches the shell. IndexedDB with the schema above and a seeded exercise library. Prove offline behaviour by turning the network off rather than by trusting the manifest. Trap: Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. Done when: With the network disabled the application opens, lists exercises and records a set that survives a reload. Reinstalling after clearing site data restores nothing, which is the behaviour that makes the export prompt necessary and is worth seeing once deliberately. Do not build yet: No routines, no timer, no charts. ### Phase 2 · Logging a workout, and a timer that does not lie Build: A live session screen: pick an exercise, log weight and reps per set, mark sets done. A rest timer started from an absolute timestamp so elapsed time is computed on wake rather than counted while awake. Large tap targets and no gesture that loses a set. Trap: setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. Done when: Starting a ninety second rest timer, locking the phone for three minutes and unlocking shows the rest as complete rather than mid-count. Every set logged during a session is present after force-quitting the application mid-session. No control needed to log a set requires two hands or a precise tap. Do not build yet: No routines and no charts yet. ### Phase 3 · Routines and prefill Build: Create routines from exercises with target sets and reps. Starting a routine creates a session prefilled with the weights and reps from the last time you did each exercise, shown as suggestions you can accept or change rather than as targets you must hit. Trap: Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. Done when: Starting a routine prefills each exercise from its most recent logged session, and an exercise never done before prefills empty rather than zero. Changing a prefilled value does not alter the historical session it came from. The wording next to a prefill describes the past and does not instruct. Do not build yet: No charts yet. ### Phase 4 · Neutral progress, enforced by a test Build: Volume over time, estimated one-rep max per exercise, session frequency and a plain session history. No streaks, no grades, no body composition scoring, no calorie targets. Add a copy lint that fails the build if a banned word appears in the interface strings. Trap: Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. Done when: The charts render from logged data with correct axes and no interpolation across gaps. The copy lint fails when a banned word is deliberately added to a string, and passes on the shipped strings. No screen displays a grade, a streak or a body composition judgement. Do not build yet: No export or sync yet. ### Phase 5 · Export and import Build: Export everything as JSON that round-trips exactly, and as CSV per table for spreadsheets. Import the JSON back into an empty installation. Prompt for an export periodically, because browser storage is not a promise. Trap: A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. Done when: Exporting, clearing all site data, and importing the file reproduces every session, set and routine exactly, verified by comparing counts and a checksum of the normalised data. The CSV opens in a spreadsheet with readable exercise names and explicit units. Do not build yet: Sync and distribution are the product phase. ### Phase 6 · Sync, devices and a safety review Build: Optional sync through a small server or a file-sync folder, with last-write-wins per set row and a visible conflict list rather than silent resolution. Then read every string in the application specifically for tone, and have someone else read them too. Trap: Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync. Done when: Two devices logging different sessions offline both end up with all sessions after sync, with no duplicates. A set deleted on one device stays deleted after sync rather than reappearing. A second person has read the interface strings and their notes are recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Wearable and watch integration. The heart rate and recovery data behind the paid tiers comes from hardware you are not building. - The social feed and following other lifters, which is the network effect and the reason the app feels alive. - Coaching, programme generation and anything that tells you what to do. That is a different product with a duty of care attached. - Any clinical or diagnostic claim. This records numbers you typed and nothing more. ### README must contain - That this is a log rather than a coach, and what it deliberately refuses to do - That browser storage can be evicted and export is the only durable copy - That estimated one-rep max is a formula, not a measurement - How the rest timer stays correct across a locked screen - The copy lint, the banned word list, and how to run it
# Architecture · Hevy Pro ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, built as an installable progressive web application | the gym has no signal and an app store review is not worth it for one user | | Storage | IndexedDB through a thin wrapper | logging a set must work offline and must never wait on a network | | Charts | Hand-drawn SVG, no charting library | four chart types with fixed requirements, and a library costs more bytes than it saves | | Timing | Absolute timestamps, never interval counters | background tabs and locked screens throttle timers, and a rest timer that lies is worse than none | | Export | JSON for completeness and CSV for spreadsheets | your training history should outlive this application | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Store | IndexedDB schema, migrations and every write. | SQLite compiled to WebAssembly is the upgrade path if queries get complex. The migration discipline matters more than the engine. | | Session | Live logging and immediate persistence. | This is the hot path and the one users feel. Optimise here before anywhere else. | | Timer | Absolute-time rest tracking and completion notification. | The timestamp approach is not negotiable. What can change is how completion is announced. | | Charts | Rendering history without interpretation. | A charting library would work, but the constraint that nothing is interpolated across gaps must survive the swap. | | Copy lint | Keeping the tone promise mechanical. | The word list grows; the build failure on violation should not become a warning. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_WEIGHT_UNIT_DEFAULT` · required · The default display unit. Storage is always canonical, so this only affects presentation. - `VITE_EXPORT_REMINDER_DAYS` · required · How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. - `VITE_SYNC_ENDPOINT` · optional · Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. - `SYNC_TOKEN` · optional, secret · Product phase only. Shared secret between your devices and your sync endpoint. ## 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 · Hevy Pro product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, built as an installable progressive web application, IndexedDB through a thin wrapper, Hand-drawn SVG, no charting library, Absolute timestamps, never interval counters, JSON for completeness and CSV for spreadsheets. - 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 - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. - Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync.
# Delivery milestones · Hevy Pro Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Offline shell and the exercise library The application opens and works with the network switched off, and logging survives a reload. ### Steps 1. Set up the project and the service worker Vite with a plugin that generates the service worker, a web app manifest with icons, and a precache of the shell. Files: `vite.config.ts`, `manifest.webmanifest` ```sh npm create vite@latest . -- --template vanilla-ts npm install -D vite-plugin-pwa ``` 2. Create the IndexedDB schema with versioned migrations One place that opens the database and applies upgrades by version. Getting this wrong later means users with a half-migrated store. Files: `src/db.ts` 3. Seed the exercise library and allow custom entries A few hundred common movements with muscle group and equipment, plus the ability to add your own. Ship the seed as a JSON file so it is reviewable. Files: `src/data/exercises.json` ### Done when - [ ] With networking disabled the application opens and lists exercises - [ ] A set logged offline survives a page reload - [ ] A custom exercise persists and appears in search - [ ] The install prompt appears and installing keeps everything working ### Watch out - Browser storage can be evicted. Safari in particular is aggressive with sites that are not installed to the home screen, and an eviction takes your training history with it. Ship export from the beginning, prompt for it periodically, and say plainly in the interface that installing the application makes storage far more durable. ## M2 · Logging a workout, and a timer that does not lie You can run a whole session one-handed, and the rest timer is right after the phone has been in your pocket. ### Steps 1. Build the session screen for one-handed use Large increment and decrement controls for weight and reps, a big done button, and no swipe-to-delete without an undo. Files: `src/session.ts` 2. Implement the timer from timestamps Store started_at. Compute remaining on each animation frame and on the visibilitychange event. Never hold the remaining value as the source of truth. Files: `src/timer.ts` 3. Write every set immediately Persist on completion of each set, not at the end of the session. A crashed browser must cost you one set at most. Files: `src/db.ts` ### Done when - [ ] A rest timer is correct after three minutes with the screen locked - [ ] Force-quitting mid-session loses no completed set - [ ] Every control is reachable one-handed - [ ] Deleting a set offers an undo ### Watch out - setInterval is throttled in background tabs and stops on a locked screen, so a rest timer built by decrementing a counter will show ninety seconds remaining after four minutes in your pocket. Store the start time, compute elapsed on every render and on visibility change, and use a notification for completion rather than relying on the tab staying alive. ## M3 · Routines and prefill Starting a familiar session takes one tap and already knows what you did last time. ### Steps 1. Build routine creation and editing Reorderable exercises with target sets, reps and rest. Keep targets optional, because plenty of people train without them. Files: `src/routines.ts` 2. Prefill from the most recent session Query the latest set rows per exercise. Distinguish clearly between no history and a logged zero. Files: `src/prefill.ts` 3. Keep history immutable Editing today's session must never write into a past session's rows. Add a test that edits a prefilled value and asserts the source session is unchanged. Files: `src/prefill.test.ts` ### Done when - [ ] A routine prefills from the most recent session for each exercise - [ ] An exercise with no history prefills empty rather than zero - [ ] Editing a prefilled value leaves the source session unchanged - [ ] Prefill wording describes the past rather than instructing ### Watch out - Prefilling from the last session and then presenting it as a goal turns a convenience into pressure. Show it as what happened last time, in neutral wording, and never highlight a lower number as a failure. ## M4 · Neutral progress, enforced by a test Charts show what happened, and the application cannot start moralising without a test failing. ### Steps 1. Draw the charts in SVG Volume per week, estimated one-rep max per exercise over time, and sessions per week. Gaps stay gaps rather than being connected by a line that implies data. Files: `src/charts.ts` 2. Write the copy lint with a banned word list A build step that scans interface strings for shaming and diagnostic language, including streak, failed, cheat, guilty, earned, burn off and body fat percentage judgements. Fails the build, not a warning. Files: `scripts/copy-lint.ts`, `scripts/banned-words.json` 3. Label estimates as estimates An estimated one-rep max is a formula applied to a set, not a measurement. Say so where it appears, so nobody treats it as a tested number. Files: `src/charts.ts` ### Done when - [ ] Charts render correctly and leave gaps unconnected - [ ] The copy lint fails on a deliberately added banned word - [ ] The copy lint passes on the shipped strings - [ ] Estimated figures are labelled as estimates wherever they appear ### Watch out - Streak counters and grade letters feel motivating for a fortnight and then quietly punish illness, travel and rest weeks. If you want a nudge, show consistency as a count of sessions in a window, which recovers naturally, rather than a streak that breaks. ## M5 · Export and import Your training history is a file you hold, not a row in someone's database. ### Steps 1. Export lossless JSON Every table, with a schema version so a future import knows what it is reading. Files: `src/export.ts` 2. Export readable CSV Join identifiers to names and include units in the header. Nobody wants an exercise id in a spreadsheet. Files: `src/export.ts` 3. Import and verify by round trip Import into an empty database and compare a normalised checksum with the source. Round-trip equality is the test worth having. Files: `src/import.ts`, `src/export.test.ts` ### Done when - [ ] Export, clear data, import reproduces everything exactly - [ ] A normalised checksum matches before and after the round trip - [ ] The CSV shows exercise names and explicit units - [ ] An export prompt appears periodically and can be dismissed ### Watch out - A CSV export that loses the unit, the timezone or the exercise identity is a spreadsheet, not a backup. Keep JSON as the lossless format and treat CSV as a convenience, and say which is which in the interface. ## M6 · Sync, devices and a safety review (production only) It works on more than one device, and someone other than you has read the words it says. ### Steps 1. Add sync with tombstones Every row carries updated_at and a deleted flag. Sync exchanges rows since a watermark. Deletions travel as tombstones, never as absences. Files: `src/sync.ts` 2. Show conflicts rather than hiding them When both sides changed the same row, keep both and show a short list. Training data is small and a person can resolve four conflicts. Files: `src/conflicts.ts` 3. Run a tone review with someone else Print every interface string and read it as though you had missed three weeks. Record what you changed. This is the phase where the rule zero promise is kept or broken. Files: `docs/tone-review.md` ### Done when - [ ] Two devices with offline sessions converge with no duplicates - [ ] A deletion survives sync rather than reappearing - [ ] Conflicts are visible and resolvable - [ ] A second reader's tone notes are recorded and acted on ### Watch out - Sets are appended far more often than edited, so last-write-wins per row is genuinely adequate here, but only if deletions are tombstones. A deletion that is simply an absent row will be resurrected by the other device on the next sync.
# Operations · Hevy Pro ## Backup The user's export file is the backup. Prompt for it on a schedule and after any large session. If you add sync, the server is a convenience and not a backup, because a bug that deletes locally will happily sync the deletion. ## Restore Import the JSON export into an empty installation. The round-trip test from Phase 5 is what makes this trustworthy, so keep it in continuous integration. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring If there is no sync server there is nothing to monitor. With one, watch for rows rejected by the watermark logic and for conflict counts, because a rising conflict rate usually means clock skew rather than genuine concurrent editing. ## Incident checklist Storage eviction is the incident that actually happens. The response is the import path, which is why it must be tested rather than assumed. For a bad migration, refuse to open the database and offer an export of the raw store rather than upgrading into a broken state. 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 - [ ] The copy lint passes and is proved to fail on a planted word - [ ] The export and import round trip matches by checksum - [ ] The rest timer is verified across a locked screen on a real phone - [ ] Sync is verified to preserve a deletion rather than resurrecting it - [ ] A second person has reviewed the interface strings for tone ## Launch constraint Do not market omitted Hevy Pro 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 default display unit. Storage is always canonical, so this only affects presentation. VITE_WEIGHT_UNIT_DEFAULT=kg # Required. How often to prompt for an export. Chosen by you; storage eviction is the reason this exists. VITE_EXPORT_REMINDER_DAYS=14 # Optional. Product phase only. Leave unset and the application stays entirely local, which is a legitimate final state. VITE_SYNC_ENDPOINT=https://sync.example.com # Optional · secret. Product phase only. Shared secret between your devices and your sync endpoint. SYNC_TOKEN=replace-with-a-random-token
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Hevy Pro because people pay for polished mobile capture, trusted content, hardware integration, and coaching rather than the basic logbook. The recurring cost buys sensor permissions, data quality, notifications, safety language, accessibility, sync, privacy, content review, and support, not just the visible interface.
xmobile apps, social features, watch integration, sync, and polished tracking
xwearable hardware data
xcoach network
xclinical validation
xlarge content library and social network
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Hevy Pro →· no votes, no pay-to-list · just what's real
Hevy Pro pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 4 routines, 7 custom exercises, and stats history limited to 30 days or 3 months depending on view; 400+ built-in exercises |
| hevy pro | $2.99/user | $2/user | Unlimited routines and custom exercises plus all-time graph/history access |
| hevy pro lifetime | custom | — | Lifetime Pro access |
free tierFree plan: 4 routines, 7 custom exercises, 30-day/3-month statistics windows, and 400+ built-in exercises.
billingmonthly + annual + one-time Lifetime; app-store offers can vary
hidden costsHevy Coach is a separate product and can create a second subscription. App-store legacy/alternate monthly offers can differ from the public $2.99 rate.
verified 2026-08-14 · source ↗
Vibecode Hevy Pro
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Hevy Pro replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Hevy Pro cost?
Hevy Pro costs about $2.99/month (Pro, checked 2026-07-31), which is $35.88 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Hevy Pro?
Honestly: mobile apps, social features, watch integration, sync, and polished tracking; wearable hardware data; coach network; clinical validation; large content library and social network. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Hevy Pro?
Yes: wger (Workouts, sets, timers and charts on your own server; setup day counts as cardio.) LiftLog (Sets, timers, plans and charts live on the phone instead of behind a lifetime unlock.) FitNotes (A no-ad Android lifting log that has been doing the basic job for years.) All 3 curated free alternatives are at vibecodeit.com/hevy-pro/alternatives. The prompt is for when you want it exactly your way.