PhysiqueAtlas
A coaching platform for contest prep — so a coach stops running the most precise sport in fitness out of WhatsApp and a spreadsheet.
Open PhysiqueAtlas →- Role
- Sole developer — schema, RLS, Edge Functions, web app, mobile app, billing
- Year
- 2026
- Status
- Live
- Stack
- React 19 · Supabase · Postgres · Expo · Stripe · TypeScript

The constraint
Contest prep is the most measured thing amateur athletes do. Weight, macros, sleep, photos, subjective readiness — logged daily, for months, against a fixed date. And it is almost universally coordinated over WhatsApp and Google Sheets.
The users are a coach on a laptop at 11pm and a client on a phone in a gym basement with no signal. One developer, no budget. Everything below follows from those two facts.
Assigning a plan deep-copies it
When a coach assigns a training or nutrition template to a client, the whole structure — days, exercises, meals, food items — is copied into independent tables. The template id is kept for reference and is deliberately not a foreign key.
The obvious design is the opposite: assignments reference templates, and you store each plan once. It’s the one a reviewer suggests. It is wrong here for four reasons that only show up later. A coach editing one client’s plan must not touch anyone else’s. Improving a template must not silently rewrite a plan someone is three weeks into. Row-level security stays a flat ownership check instead of a join back through templates. And a log stays meaningful even after the plan it came from is edited.
What it cost: real data duplication, and template improvements don’t propagate — a coach has to reassign to push an update. That’s a worse story on a feature list and a better one in production.
The database is the authorization layer
All access is scoped by Postgres row-level security keyed on the authenticated
user. The apps add no coach_id filters of their own; they are explicitly
forbidden from doing so.
The tempting alternative is to filter in the application, where it’s easy to read and easy to test. But then every new query is a new opportunity to forget, and the two clients — web and mobile — have to agree forever. Putting it in the database means a missed filter returns nothing instead of everything.
What it cost: policies are genuinely hard to test and harder to review, they can only be exercised against a real Postgres with a real role, and a mistake is a data leak rather than a 500. That is the trade, taken deliberately.
Shipping to a store ends lockstep, permanently
This is the decision I’d defend hardest, and it is about a constraint rather than a feature.
Before the first store release, the web app, the mobile app and the database deploy together. Migrations and the front end ship in one action; testers reinstall on demand. A store release ends that forever. A user controls when they update, neither store lets you withdraw a version out from under existing installs, and someone will be running a build from four months ago.
So from the first submission onward: migrations must stay backward compatible with the oldest supported build. No dropping or renaming a column or an RPC parameter that a released app still reads — expand first, contract a release later, where “a release later” is measured in store adoption, not in merges. An RPC signature change is a breaking change; you add an overload, you don’t mutate the one an in-flight build is calling. And a server-checked minimum-version gate has to exist before the first submission, because without it the only lever against a stale install is freezing the schema.
What it cost: every schema change now carries a compatibility question the pre-store repo could ignore, and every removal is a two-step. That is the price of shipping to a store instead of a tester group. The alternative — assuming lockstep forever — silently breaks real people’s apps.
Where it stands
Live, in beta with coaches, and registration is invite-only. Pricing is decided but not switched on: the tiers exist in Stripe and the public page deliberately still shows no numbers until there is a willingness-to-pay signal to set them against.
What I’d do differently
I wrote the compatibility contract before the first store submission, which was the right order — but only because a near-miss forced the thought. Two RPC signature changes had already shipped safely, and they were safe purely because nothing was in a store yet. I’d rather have reached that conclusion by design than by noticing how close I’d come.
Screens
Captured from the running app against seeded demo data — the people and figures in them are fictional.


