PrimeFit

Offline-first gym management PWA

The gym management platform I founded and run: memberships, retention and a training app that keeps working when the gym's Wi-Fi drops. In production since 2024 with a founding gym, 130+ members and 70 daily active users.

Role: Founder & Full Stack Developer: product, design, code, infrastructure and support
Duration: 2024 - Present
PrimeFit hero image

Screens

PrimeFit member home with today's routine and weekly streakPrimeFit workout in progress, logging sets and reps on a phonePrimeFit weekly volume analytics per muscle groupPrimeFit interactive body heatmap

Tech Stack

React 18 TypeScript Vite Redux Toolkit Dexie Workbox Supabase PostgreSQL Tailwind CSS shadcn/ui Vitest

Overview

PrimeFit didn't start as a client project. It started as a problem I kept seeing in gyms and independent coaches: the coach masters the training, but the operation lives scattered across spreadsheets, static PDFs and chat threads. Routines get lost, members plateau without anyone noticing, and a member who feels ignored doesn't complain, they just stop showing up. I built it with a founding gym in Puntarenas, Costa Rica, whose owner used to lose four hours every Sunday building spreadsheets and PDFs, and I designed it for how it is really used: on a phone, between sets, with one hand.

⚡ The Challenge

A gym floor is a hostile place for a web app. The signal drops mid-workout, phones lock between sets, and iOS purges localStorage after a couple of weeks of inactivity. Losing a logged workout is the one bug members never forgive. On top of that the platform is multi-tenant: a gym must never see another gym's members, and a suspended gym has to actually stay suspended.

✓ The Solution

Reads go through a Dexie (IndexedDB) cache hydrated at login; writes go through a durable queue. Every queued operation carries an idempotency key sealed to the workout session, and the server enforces it with a unique index, so a retry can never create a second workout. A workout finished offline gets a local id; when it syncs, the operations that depend on it are remapped to the real id, even across app restarts. The service worker updates itself, but the reload is deferred while a workout is in progress or the queue still has pending operations. On the server, every table is protected by row-level security keyed by gym, and writes go through SECURITY DEFINER RPCs.

Impact & Results

~2 years
In production

Running since 2024 with a founding gym in Puntarenas, Costa Rica

130+
Registered members

With 70 daily active users logging workouts in the app

135
Test files

Vitest and React Testing Library plus database-level integration tests

Key Decisions

1

A durable queue instead of Redux-Persist

iOS purges localStorage after 7 to 14 days of inactivity and it has no transactions, so persisted Redux state was demoted to a single slice (the workout in progress). Everything else moved to Dexie, with versioned schema migrations.

2

Idempotency keys sealed to the session

The first version generated a new key on every retry, so the unique index protected nothing. Keys are now created once per workout session and the database rejects duplicates. Nine kinds of operations are queued, from finishing a workout to confirming a membership payment.

3

A failed sync is not always a failure

Operations get five attempts, but two cases don't burn one: an operation that arrived before its parent, and an expired session. Before that rule, losing the session with the app open emptied the queue in five polling ticks and told the member their workout was lost.

4

Zero rows is not success

PostgREST answers 204 with no error when RLS or a soft delete makes an UPDATE miss. The queue would mark the operation as done and the data evaporated. Every write now selects the id back and throws on zero rows; a sweep found the same bug in five other features.

5

Deferred service-worker updates

In July 2026 every client froze on an old bundle: the service worker was set to prompt and no UI ever prompted. It now updates automatically, but the reload waits until no workout is live and the queue is empty, and a stale client that hits a missing lazy chunk reloads itself at most once every 30 seconds.

6

One public door for share links

Coaches can share a read-only routine with no account. Instead of adding a policy for anonymous users, a single SECURITY DEFINER function with a hand-written column allow-list is the only public entry point, and every failure mode returns the same NULL, so there is nothing to probe.

Technical Highlights

Key Learnings

Offline-first is a product decision, not a caching flag. The hard part is not storing data, it is deciding what a retry means.

A guarantee that only lives in the client is not a guarantee. The unique index is what actually prevents duplicate workouts when two tabs are open.

Test the failure you fear on purpose. The hostile offline suite found nine ways to lose data silently that no happy-path test would ever hit.

Suspending a tenant has to be enforced somewhere real. Column-level grants are as much a part of multi-tenancy as row-level policies.

Designing with one real gym owner, phone in hand between sets, settled more UI debates than any mockup.

View all case studies