Hybrid strength, forged daily.
A mobile-first PWA — Hyrox athlete journal with rehab tracking, daily wellness metrics, and AI-driven weekly summaries. Postgres on Neon behind Cloudflare Workers; AI insights powered by Claude.
I train for Hyrox (Sub-65' Gdańsk Oct 2026 target), am currently rehabbing a tendon injury, and follow a structured weekly cycle (PON Hyrox, WTO siłowy, ŚRO rehab, CZW siłowy, PT Hyrox z fizjo, SOB long Z2/3, ND reset). My training notes — multi-paragraph reflections, EMOM/AMRAP/WORK blocks, Sleep Score / HRV / HR Rest, rehab protocols A/B with meds and USG — live in scratch documents. Forge replaces that with an app that:
- logs sets fast on the gym floor,
- structures every training format I actually do (strength + Hyrox formats + compromised run + cardio zones + rehab),
- tracks rehab as a first-class domain (Protokół A: Foot/Ankle, Protokół B: VMO, injuries timeline, pain check-ins, meds),
- captures daily wellness (Sleep, HRV, HR Rest, weekly volume),
- uses Claude AI to generate weekly summaries (strength progress, average HR per zone, sleep trends, session-to-session comparisons),
- supports athlete↔coach sharing and minimal "other athletes" view (P1/P2),
- imports Garmin data via manual
.FITupload (P1) and the official Garmin Health API once registered.
A second goal: train software engineering and product/architect skills on a modern 2026 stack.
Original (pre-audit) scope was "generic training PWA for strength + running with daily notes". After a multi-agent audit on 2026-05-16 captured the actual use case in /tmp/forge-user-notes.md (and the resulting ADR-0009), the scope was re-framed as above.
| Layer | Choice |
|---|---|
| Frontend | TanStack Start (Vite + React 19), TanStack Router/Query; React Hook Form + Zod for forms |
| UI | shadcn/ui (radix-nova) + Tailwind v4, Vaul (mobile drawers); animation library TBD |
| Charts | Tremor / Recharts for dashboards; visx for race recap (P1) |
| Backend | Server functions (TanStack Start) on Cloudflare Workers |
| Database | Postgres on Neon + Drizzle ORM + pgvector (RAG) |
| Auth | Better Auth (email magic link via Resend + Google OAuth); multi-tenant schema from day 1 |
| AI | Anthropic SDK (Claude Sonnet 4.6 default, Opus 4.7 for weekly summaries), Vercel AI SDK, Deepgram for STT (PL), pgvector for embedding-based similar-session search |
| Wearable | Manual .FIT upload (P1) + Garmin Health API once registered (P2); enum ImportSource future-proofs swaps |
| PWA | Vite PWA Plugin — manifest only (installable to home screen); no service worker by design (ADR-0024) |
| Hosting | Cloudflare Workers (ADR-0017); Neon (Postgres) |
| Observability | Sentry + PostHog (events only — never reflection text or pain notes; health-adjacent stays out of analytics) |
| Tooling | Bun, Biome, Bun test + happy-dom + Testing Library, Playwright (post-MVP), Storybook (post-MVP), husky + commitlint + knip |
See docs/architecture/system-overview.md for the system overview, docs/architecture/data-model.md for the schema, and docs/adr/ for architecture decisions (see the index for the current list).
Feature-first layout (Bulletproof React-style, pragmatically applied — see ADR-0018 for the full decision + rationale + alternatives considered).
src/
routes/ # TanStack Start file-based routing. Thin (~20 lines):
# createFileRoute + import <View /> from features/.
features/<feature>/ # All domain code for one feature. Folders are named
# by RUNTIME — where the code executes:
server/ # Server-side code: createServerFn RPC endpoints
# (one file per resource), library instances
# (Better Auth), server-only helpers. Files
# plain-named, NOT *.server.ts — that extension
# triggers TanStack Start's import-protection and
# breaks the RPC split.
client.ts # Client-only non-React code (browser SDK wrappers).
# Promote to client/ folder when it outgrows a file.
views/ # Top-level entry components. Imported by exactly
# one route. Naming: <Name>View.tsx.
components/ # Reusable building blocks within the feature.
forms/ # Vertical form slices: Zod schema + RHF + submit.
lib/ # Universal non-React helpers (one per file).
constants.ts # Enums, label maps.
types.ts # Feature-scoped TypeScript types.
shared/
components/ # Cross-feature business components (Spinner,
# StatusBadge — used by 2+ features, have business
# semantics, not pure UI primitives).
components/
ui/ # shadcn primitives. Pure UI, no business knowledge.
lib/ # Cross-cutting non-React utilities (cn, env, session).
Three-tier component model: UI primitive (components/ui/) → cross-feature shared (shared/components/) → feature-specific (features/<feat>/components/). A view is what a page renders; a component is what a view is built from. Routes stay thin.
Work is tracked in Linear (initiative Forge). PRs link to Linear issues via the ## Linear section in the PR template (see .github/PULL_REQUEST_TEMPLATE.md).
Workflow is hybrid (since 2026-05-16, ADR-0011):
- Small changes (typos, single-file tweaks): fast-track without Linear ceremony.
- Epic-level work: Linear issue + branch
feat/frg-N-<short-scope>+ PR with self-review.
Release flow: main (DEV) → staging (STG) → production (PROD), automated via .github/workflows/linear-release.yml (sync issues to release stages on each push).
Requirements: Bun ≥ 1.3 and Node ≥ 22.
bun installForge uses Neon as managed Postgres. Sign up (Google OAuth is fastest), create a project called forge, and copy the pooled connection string from the Connection Details panel.
Then create a local .env (gitignored) from .env.example and paste the connection string:
cp .env.example .env
# Edit .env, set DATABASE_URL to your Neon connection string.Run the initial migration + catalogue seed (Hyrox stations, exercises with PL aliases, rehab protocols, demo athlete, long-term goals):
bun run db:migrate # applies db/migrations/*.sql to the DB
bun run db:seed # idempotent — seeds catalogue + demo athleteTo seed under your own identity instead of the placeholder "demo athlete", set in .env:
SEED_DEMO_EMAIL="you@example.com"
SEED_DEMO_NAME="Your Name"
SEED_DEMO_USERNAME="your-handle"Personal race results (Hyrox splits with real times) are seeded separately via the gitignored db/seed-personal.local.ts — copy db/seed-personal.local.example.ts to that filename and fill in your data. Public repo never sees real race data.
bun devDev server runs at http://localhost:3000. Styled with Tailwind v4 + shadcn/ui (radix-nova preset).
The app exposes a Web App Manifest, so it's installable to the phone home screen ("Add to Home Screen"). That is the full PWA scope by design — no service worker, no offline shell (see ADR-0024).
bun dev # start dev server (Vite + TanStack Start)
bun build # production build
bun start # serve the production build
bun test # run unit tests (Bun test + happy-dom + Testing Library)
bun test:watch # watch mode
bun run typecheck # tsc --noEmit
bun run lint # Biome
bun run format # Biome --write
bun run knip # detect unused exports / deps / files
bun run check # lint + typecheck + test + knip (full pre-PR gate)
bun run db:generate # generate SQL migration from db/schema.ts diff
bun run db:migrate # apply pending migrations to DATABASE_URL
bun run db:push # sync schema directly without migration (DEV ONLY — destructive)
bun run db:studio # open Drizzle Studio (GUI at https://local.drizzle.studio)
bun run db:seed # seed catalogue + demo athlete (idempotent)See CONTRIBUTING.md for branch naming, ADR process, and the hybrid workflow rules.
MIT — see LICENSE.