Case study ยท IronPulse

A fitness platform that ships when I’m not watching it.

IronPulse is a self-hosted fitness platform that combines strength training, GPS cardio, health metrics, social, and coaching — across web and native mobile, with offline-first sync. It’s also a sandbox for how I work now: monorepo, tRPC end-to-end, autonomous deploys triggered by an agent.

Role
Solo build — architecture, web, mobile, deploy
Stage
2025–present
Codebase
~50 packages, Turborepo + pnpm workspaces
Why it’s here
End-to-end scope, modern stack, real shipping

The shape of the thing

IronPulse is a monorepo with two apps and four shared packages. The web app is Next.js 15 on React 19 with the App Router; the mobile app is React Native 0.81 (bare CLI, not Expo) running natively on iOS and Android. Both talk to the same tRPC 11 API with 24 feature routers, which lives next to a Prisma 6 schema against PostgreSQL 16 with the PostGIS extension for cardio routes.

  • Next.js 15
  • React 19
  • React Native 0.81
  • TypeScript
  • tRPC 11
  • Prisma 6
  • PostgreSQL 16
  • PostGIS
  • PowerSync
  • Redis 7
  • NextAuth v5
  • WebAuthn / passkeys
  • Stripe
  • Tailwind CSS
  • Radix UI
  • Sentry
  • Playwright
  • Maestro
  • Turborepo
  • pnpm workspaces

Offline-first, mobile-first

For a fitness app, the network has to be optional. Workouts get logged in a gym lift in the basement; runs happen in places without cell coverage. IronPulse uses PowerSync with a local SQLite store on mobile (and IndexedDB on web) — every write is local first and reconciles upstream when the connection returns. The native mobile build reads HealthKit (iOS) and Google Fit (Android) directly so heart rate, weight and steps flow in without a separate sync layer.

What that gives users: open the app on a treadmill in a no-signal gym, log a full session, leave. The next time the device hits Wi-Fi, sets and reps land on the web dashboard without a single tap. No “tap to sync” UI, no merge conflicts, no spinner.

Authentication that doesn’t pretend it’s 2014

Auth runs on NextAuth v5 with passkeys (WebAuthn) as the primary credential, plus Google and Apple OAuth. Passwords are still available for the slow majority of users who want them, but the default flow on a new device is a passkey prompt — one tap to enroll, one tap to sign in.

Multi-tier billing, designed once

Stripe handles three subscription tiers: a free plan, an Athlete tier with the full data set and devices, and a Coach tier that exposes a client roster and program-authoring tools. The billing surface lives in a single tRPC router and one set of Prisma models so the upgrade flow, feature gating, and webhook handlers all read the same source of truth. Adding a new tier is roughly an enum value, a row in the entitlements table, and a feature flag.

An agent that ships releases for me

Tagged releases trigger an autonomous DevOps agent (running on my homelab) that builds the web image, deploys it to the Oracle ARM VM, runs a Lighthouse smoke test, and posts the result to a private Signal thread. The SSH key it uses is forced into a deploy wrapper that only accepts strict-semver tag arguments — the agent literally cannot do anything but deploy a tagged release. If a build fails Lighthouse perf budgets, it rolls back and pings me before going near production.

Where it goes next

IronPulse is private alpha while I’m wiring up coaching workflows and the first social features. The architecture is built to handle that without re-platforming — tRPC means I can add a router per feature and both clients pick it up with full type safety; PowerSync handles new tables as they appear in the schema.

Back to selected work  ·  Talk to me about a similar project