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.
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.


