Skip to content
All projects

Personal productJun 2026 to presentPre-launch

ONYX

An offline-first powerlifting log that does the math between sets

  • React
  • TypeScript
  • Fastify
  • PostgreSQL
  • Astro
Live appLanding site

The source code is private.

At a glance

What
ONYX, a powerlifting training log for Brazilian lifters. Free, in Portuguese, works offline.
Role
Solo project: product, design, frontend, backend, data and deployment.
When
June 2026 to present.
Stack
React 19, TypeScript and Vite (PWA) · Fastify 5, PostgreSQL 16, Drizzle ORM and Zod · Astro 5 · Docker, Nginx and GitHub Actions.
Status
Deployed at onyxtreino.com.br, before its public launch. No usage numbers yet.

The problem

Lifters who train squat, bench press and deadlift three to five times a week usually log their training in a spreadsheet or in their coach's PDF. Between sets, they do the math by hand: today's load as a percentage of their max, an estimated max from reps and effort, the plates to load on the bar.

Generic apps such as Strong and Hevy are in English and don't speak the sport's language. None of them compares a lifter's strength with people who actually competed in Brazil.

My role

I built ONYX alone, from the product definition to production. That covers the product decisions, the interface and design system, the React app, the API and database, the data pipeline and the deployment.

I work one issue at a time: each change starts as a written issue, gets a plan, and ships as one focused pull request that merges only when lint, tests and build pass. I use AI coding agents (Claude Code and GitHub Copilot) to plan and implement those issues, following conventions documented in the repository, and I review every change.

What I built

  • Logging made for the gym floor: sets with weight, reps and RPE; warm-up, working and drop sets; a rest timer; a plate calculator inside the set row.
  • Training math: estimated 1RM from RPE using the RTS table, with Brzycki as a fallback; DOTS, Wilks and IPF GL scores; loads prescribed by %1RM or by RPE.
  • Routines and programs: built-in templates such as Wendler 5/3/1 and Madcow 5x5, plus the lifter's own.
  • Progress: analytics, history, records and a calendar, built only from what the lifter logged.
  • "How strong are you": percentiles against Brazilian raw competitors from the last ten years, from OpenPowerlifting's public data.
  • A public landing site: free calculators that work without an account and carry the result into sign-up.

Architecture

ONYX is a monorepo with three apps and a shared package. The React app is a PWA that saves everything on the device first and syncs in the background with a Fastify API backed by PostgreSQL. The Astro landing site reuses the app's calculation functions, so a result on the public calculator matches the app. The API's validation schemas are checked against the shared domain types at compile time.

Landing (Astro)onyxtreino.com.brFree calculators,no account neededApp (React PWA)app.onyxtreino.com.brSaves to localStoragefirst, works offlineAPI (Fastify)JWT and Google sign-inSync: last-write-winsZod-validated contractsShared TypeScript codeDomain types and pure formulas: e1RM, DOTS, Wilks, IPF GLPercentile table precomputed from OpenPowerlifting dataPostgreSQL 16Drizzle ORM migrationsSoft deletes, tombstonessign-upsyncshared types
The app saves on the device first, and the API reconciles devices. The landing site and the app share the same formulas, so a result on the public calculator matches the app.

Key decisions

DecisionWhyTrade-off
Offline-first: save locally, sync in the backgroundGyms have weak signal, and logging a set should never wait for the networkResolving conflicts between devices becomes the app's job
One sync rule for every entity: last-write-wins by update time, with soft deletes and tombstonesSimple to reason about and to testWhen two devices edit the same item, only the newest edit survives
Shared domain types, with the API's Zod schemas checked against them at compile timeA field added on the client can't be silently dropped by the APIEvery new field touches two places in the same pull request
Calculations as pure functions shared by the app and the landing siteOne source of truth for every formula, easy to unit testThe landing build depends on the app's code
Percentiles precomputed from a public data dump and refreshed by hand every quarterNo runtime dependency or cost, and the data stays attributableNumbers can be up to a few months old
No CSS framework or animation libraryFewer dependencies to maintain and full control of the design systemMore hand-written CSS

Challenges

Sync that lost data. The first version of sync treated workouts as append-only and let each device send its full list of custom exercises. In production, that lost data in two ways: edits and deletions of workouts were discarded, and a device with an old list could permanently delete exercises created on another device.

I replaced it with one contract for every entity: last-write-wins by update time, inside a database transaction. Deletions became soft deletes and tombstones, so a late push from an old device can't bring a deleted workout back. The decision logic lives in a pure module, tested against the exact cases that had lost data, plus integration tests for the sync route.

Numbers the app can't back up. A training app is tempting to fill with charts and comparisons from day one. ONYX shows only what the lifter's data supports: with no history, it says it doesn't know yet. Totals and estimated maxes say whether they are current or all-time bests, and scores like DOTS appear only after bodyweight and sex are set.

How I know it works

  • 500+ automated test cases: calculations, sync merging and the data pipeline (Vitest), API routes (Node's test runner), and end-to-end tests of the main flow and the sign-up handoff (Playwright).
  • Every pull request runs lint, tests and build for the app, the API and the landing site. Deploys run the same gate again, then a smoke test against the live app and API.
  • Accessibility rules in the design system: WCAG AA contrast, 40 px touch targets, 16 px inputs, and reduced motion respected.

Results

ONYX runs in production at onyxtreino.com.br but hasn't launched publicly yet, so this case claims no usage numbers. What it does show:

  • Four months of continuous delivery: more than 180 merged pull requests by October 21, 2026.
  • Cross-device sync that went through a real data-loss bug and is now covered by tests.
  • A data pipeline that turns the full OpenPowerlifting dump (about 800 MB of CSV) into a small percentile table, with attribution.

What I learned

  • Design sync for the worst device, not the typical one. My first version of sync assumed every device was up to date, and a device with an old list deleted exercises created on another. Now every sync rule assumes a device can be late, offline or out of date.
  • "I don't know yet" builds more trust than a pretty chart. With little data, it's tempting to show progress anyway. ONYX hides scores and trends until the lifter's own history supports them, and says so.
  • AI agents are only as good as the conventions you write down. I use coding agents throughout the project. What keeps their work reviewable is a documented architecture and set of rules, and one issue per pull request.
  • Subtraction made the product clearer. Removing decorative effects from the landing site and the app made the numbers and the main actions easier to read at a glance.