Workout is a local-first web app for building, running, and tracking workout sessions. It works as a public guest tool in the browser, and can also unlock a private SQLite-backed mode with a simple password.
- Exercise library with add, edit, delete, categories, notes, and defaults.
- Drag-and-drop workout builder.
- Timed, repetition-based, and distance-based workout steps.
- Optional per-step weight tracking.
- Rounds, breaks, 5-second get-ready countdown, active timer, pause, resume, stop, partial save, and completion flow.
- Quick interval timer with saved last-used work/rest/round settings.
- Completed workout history with manual entry, workout/exercise filtering, and partial session tracking.
- Statistics page with weekly/monthly/yearly workout counts and a first lifting-progress view for weighted reps.
- English and French interface.
- Audio modes for local Piper TTS, browser voice, beeps, and silent workouts.
- Separate app language and spoken announcement language.
- Character page with built-in avatars or an uploaded photo.
- Guest mode using browser localStorage.
- Local mode JSON export/import for moving browser data between devices.
- Private mode using SQLite through the Node API.
- Password login with Argon2 hash support.
- PWA install support with basic static app-shell caching.
- Docker deployment on port
8060.
Workout has two separated storage modes:
| Mode | Who uses it | Storage | Privacy behavior |
|---|---|---|---|
| Local mode | Visitors before login | Browser localStorage under workout.guest.* |
Never reads private API data |
| Private mode | Logged-in owner | SQLite through /api |
Protected by password session cookie |
| Server mode | No password configured | SQLite through /api |
API is open on the deployed app |
Guest/local data is not automatically imported into the private database. This keeps visitor experiments separate from the owner database.
Local mode includes JSON export/import from the top navigation so browser-only data can be backed up or moved to another browser.
- React 18
- TypeScript
- Vite
- Tailwind CSS
- dnd-kit
- Lucide React icons
- Node 22
- SQLite via
node:sqlite - Argon2 password verification
- Piper TTS with cached generated audio
- Web app manifest and service worker
- Docker Compose
Install dependencies:
npm installStart the Vite dev server:
npm run devBuild the production frontend:
npm run buildRun the production Node server:
npm startDefault local URL:
http://localhost:8060
| Command | Purpose |
|---|---|
npm run dev |
Start the Vite development server |
npm run build |
Type-check and build the production frontend |
npm start |
Serve dist and the API with Node |
npm run preview |
Alias for the production Node server |
npm run api |
Run the Node API/static server |
npm run seed:demo |
Add demo plans and history to the configured SQLite database |
Login is enabled only when WORKOUT_PASSWORD_HASH is set in .env or the server environment.
Generate an Argon2 hash with Python:
python -c "from argon2 import PasswordHasher; import getpass; print(PasswordHasher().hash(getpass.getpass('Password: ')))"Or generate one with the project Node dependency:
node --input-type=module -e "import argon2 from 'argon2'; import readline from 'node:readline/promises'; import { stdin, stdout } from 'node:process'; const rl = readline.createInterface({ input: stdin, output: stdout }); const password = await rl.question('Password: '); rl.close(); console.log(await argon2.hash(password));"Create .env:
WORKOUT_PASSWORD_HASH='$argon2id$...'
WORKOUT_AUTH_SECRET=replace-with-a-long-random-string
Quote WORKOUT_PASSWORD_HASH because Argon2 hashes contain $.
WORKOUT_AUTH_SECRET signs the HTTP-only session cookie. If it is omitted, the password hash is used as the signing secret.
Generate a random cookie secret with Node:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"- Real secrets must stay in
.envor the server environment. .env,.env.local,data/,dist/, and test logs are ignored by Git..env.examplecontains placeholders only.- The repository does not contain a real password hash, plaintext password, token, API key, or private key.
- Private API data is never mirrored into guest localStorage.
- The login cookie is
HttpOnly,SameSite=Lax, and can be marked secure withWORKOUT_COOKIE_SECURE=true. - If the app is served over HTTPS, set
WORKOUT_COOKIE_SECURE=true.
The production container:
- Builds the React app into
dist. - Serves static files and
/apifrom Node. - Stores SQLite data in
/data/workout.sqlite. - Generates and caches Piper TTS audio in
/data/tts-cache. - Exposes port
8060.
The first Docker build downloads Piper and the bundled English/French voice models, so that build can take longer than later rebuilds.
Start or rebuild:
docker compose up -d --buildThe compose file mounts:
/srv/webdata/workout:/data
So the host database lives at:
/srv/webdata/workout/workout.sqlite
Cached generated voice files live at:
/srv/webdata/workout/tts-cache
Create the host directory if needed:
sudo mkdir -p /srv/webdata/workoutSeed sample workout plans and completed history:
npm run seed:demoInside Docker:
docker compose run --rm workout npm run seed:demoThe seed command creates 3 demo plans and 11 completed sessions. It replaces only rows with demo- IDs and keeps real data intact.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/health |
Public | Health check |
GET /api/auth/status |
Public | Check auth/session status |
POST /api/auth/login |
Public | Verify password and set session cookie |
POST /api/auth/logout |
Public | Clear session cookie |
GET /api/data |
Private when login is enabled | Load all app data |
POST /api/import |
Private when login is enabled | Replace all app data |
GET /api/tts/status |
Private when login is enabled | Check local Piper TTS availability |
POST /api/tts |
Private when login is enabled | Generate or reuse cached Piper speech |
GET /api/tts/audio/:file |
Private when login is enabled | Play cached generated speech |
PUT /api/exercises |
Private when login is enabled | Save exercises |
PUT /api/plans |
Private when login is enabled | Save workout plans |
PUT /api/sessions |
Private when login is enabled | Save history |
PUT /api/settings |
Private when login is enabled | Save settings |
server/
index.js Node server, SQLite storage, auth, static serving
defaultData.js Default exercises and settings
seedDemoData.js Demo data seeding command
src/
app/ App shell and mode switching
components/ UI components
data/ Local/server storage abstraction
hooks/ React state hooks
i18n/ English/French translations and exercise names
models/ Shared TypeScript data models
services/ Speech, auth, and workout engine services
styles/ Tailwind entry CSS
The in-app About popover shows:
This site uses Workout, a project by Ketah.
Workout links to https://github.com/erille/workout.
- Deeper lifting analytics and body-weight-aware calculations.
- Better generated audio cache management.
- More workout templates.
- Optional richer voice engine.