Go Back

Case study

Loadline for iOS

An offline-first strength training app designed and built end to end.

Live · 1,849 registered users
Visit project

Tech stack

  • React Native
  • Expo
  • TypeScript
  • Swift
  • SQLite
  • Supabase
  • PowerSync
  • Sentry
  • Claude Code
  • Codex CLI
Loadline campaign artwork reading Actually see if your training is working, surrounded by the dashboard, workout and analytics screens

Fast workout logging, with or without a connection

Loadline is a strength training app for people who want the speed of a notebook along with structured history and useful analytics. During a session, it handles previous set values, supersets, unilateral work, rep ranges, RPE or RIR, automatic rest timers and different exercise measurement types. Once the workout is done, the same data powers personal records, estimated strength trends, consistency views and plateau detection.

I designed and built the product from end to end. That work covered the React Native app, its local data model, custom Swift integrations and the Supabase infrastructure behind it. I also handled observability, subscriptions and App Store releases. Owning the whole product meant that problems found in real workouts fed directly back into the design and engineering.

Loadline feature overview showing the dashboard, fast workout logging, plateau detection, muscle-group analysis, workout sharing and Health integration
Live workout logging and the progress views built from the same training data.

Built for unreliable gym Wi-Fi

Workouts last a while and require constant input. They also happen in basements and busy buildings where reception comes and goes. If the connection drops halfway through a session, the user must still be able to see their history, log the next set and finish the workout without waiting for a spinner.

When the user saves a set, it has to be safe immediately, even without a connection.

SQLite owns the workout loop

Each device keeps a user-specific SQLite working set with 35 tables, backed by OP-SQLite. This local database is the durable source of truth. Kysely provides typed access, and PowerSync watch queries notify the app when the replica changes. Zustand is limited to active workout state and derived snapshots for the interface.

A completed set goes into SQLite first, so the interface responds immediately whether the phone is online or offline. PowerSync manages the transactional mutation queue and writes server changes back into the local replica. A custom connector maps those operations to Supabase, batches compatible writes and handles failures. Media has its own durable queue because file transfers need different retry, integrity and cleanup rules.

Interaction layerReact Native interfaceNative routes, live workout input and immediate local feedback
Interaction stateZustandActive-workout state and derived, view-ready snapshots
Durable local source of truthOP-SQLitePowerSync maintains the replica; Kysely provides typed access
Cloud and reconciliationPowerSync · Supabase · PostgresOrdered uploads, server-originated updates, authentication and file storage

What offline support changes

ConcernTypical API-first appLoadline
The user logs a setSend it to the API and waitWrite it to SQLite immediately
The connection dropsFall back to a read-only cache or an errorKeep the whole workout readable and editable
A write fails repeatedlyKeep retrying the requestLimit retries, preserve the payload and unblock the queue safely
The data model changesUpdate the API and client typesKeep Postgres, sync rules, SQLite and TypeScript in sync
The app is not openStop the interactionContinue through Live Activities, rest timers, quick actions and HealthKit

Three failures that changed the design

  • Open offline without showing stale or empty screens

    The first version of the startup flow waited for Supabase to validate the session before restoring the local user. With poor reception, someone returning to the app could sit on the splash screen even though their workout history was already available in SQLite.

    Startup now restores the cached user and opens SQLite synchronously. Supabase validates or refreshes the session in the background. If the account changes, the app closes the previous user's subscriptions and clears user-specific stores before loading queries for the new user.

  • Prevent one bad mutation from blocking the upload queue

    PowerSync queues offline writes in transaction order, which keeps them consistent. It also creates a difficult failure mode: one malformed or permanently rejected write can hold up every workout behind it.

    I built the upload policy around that queue. It batches compatible writes, separates retryable failures from deterministic ones and gives each operation a retry budget. Before a poison transaction leaves the blocking path, the app saves its full payload to a server-side dead-letter store. If that save fails, the queue stays blocked until Sentry has flushed a recovery artifact. I can then replay the payload or generate reviewed SQL to reconcile the data.

  • Keep a growing workout model from losing information

    A workout set started with weight and reps. Over time, Loadline added RPE and RIR, unilateral values, rep ranges, timed and distance work, supersets, personal records, programs and training blocks. With that many shapes, one field missing from a mapper could corrupt someone's history or reopen a routine differently from how it was saved.

    Shared repositories and mutation gateways now own the reads, codecs and writes. Parity and architectural drift checks catch mismatched schemas, while integration tests cover the full roundtrip from workout to routine and back. At startup, the app fingerprints the on-device schema. If an older binary finds an incompatible version, it can rebuild the PowerSync replica from Postgres instead of trying to repair a local shape it no longer understands.

Using iOS where it improves the workout

Most of Loadline is built with Expo and React Native. Features that need deeper access to iOS cross into Swift, which keeps the main product fast to develop without giving up native behavior where it helps the workout.

  • ActivityKit Live ActivitiesThe current exercise, set, workout clock and rest timer stay visible outside the app. Payload validation and resume recovery keep the activity in sync with the workout.
  • HealthKit integrationThe app reads and writes body metrics and workout samples. It processes anchored changes in the background without duplicating its own records.
  • iOS-native interactionNative toolbars, menus, form sheets and Home Screen quick actions work alongside a custom workout keyboard and responsive rest timer controls.
  • Safe releasesRuntime version checks stop OTA updates from reaching incompatible native binaries. A fingerprint guard catches unversioned native changes, and staged rollouts reduce the number of users affected by a faulty release.

What changed with real users

1,849

registered users have used Loadline in conditions that are hard to reproduce in development.

Real usage exposed cases that were easy to miss during development. Tokens expired while the phone was offline. Date-sensitive subscriptions crossed midnight. HealthKit sent a duplicate sample, Storage lost a photo and a native activity received an old payload. Fields also drifted between saved routines and completed workouts. I turned those incidents into recovery paths, contract tests and architectural checks.

Building Loadline made me comfortable working at both ends of a product. I can refine a two-pixel interaction detail, then trace the recovery path that protects someone's training history.

What I would simplify next

Dependable offline support reaches far beyond the database. Startup, account changes, queued writes, deletes, attachments and server changes all need to work without a stable connection. The same is true for observability and recovery. Ignoring any of those paths leaves an app that works in a demo and fails in someone's hand.

If I started again, I would add shared mutation gateways and schema parity checks much earlier. I would also keep the sync boundaries narrower. The current stack works, but coupling between its layers makes infrastructure changes expensive. Complexity that protects the user's data has a clear job. Complexity left by earlier decisions is a candidate for removal.