Duetro: a workout log built for two

An iOS app for couples who train together on different routines, with a live view of each other’s workout from across the gym.

iOS

  • Swift 5
  • SwiftUI + Observation
  • ActivityKit + WidgetKit
  • Swift Charts
  • SQLite
  • Sign in with Apple

API

  • TypeScript
  • Hono on Cloudflare Workers
  • Durable Objects + WebSockets
  • Prisma
  • Postgres on Neon via Hyperdrive

Testing

  • SwiftPM package over the shipping files
  • Eight API suites, one over a WebSocket
  • A design gallery inside the app

Project overview

Team: just me. Product, design, iOS and backend

Created at: Side project, August 2026 to present

What I worked on:

SwiftUI app

Design system

Realtime presence

Offline-first sync

Workers API

Live Activities

My girlfriend and I train together, but never on the same routine, so every session meant craning across the gym to see what set the other was on. Duetro is the app I built so we could stop craning.

Duetro: log together, grow together

Duetro logo

The challenge:

Two people, two phones, two routines. Partners rarely lift the same weight, gym Wi-Fi is hostile, and nobody wants to tap refresh between sets.

So the rule became: share the moment, never the numbers.

A pair of colours

You’re blue or pink, and your partner is always the other one, on both phones.

Watch from across the gym

A live card lands on Home when they start. One tap swaps between their lane and yours.

Cheer, then say it

Cheers burst where you throw them. A note arrives as you type it.

Log with one thumb

Weight, reps, tick. Rest starts itself, and the plates draw themselves.

Finish, together

One sheet to confirm, then both axolotls celebrate.

Progress for two

The week for both of you, then every lift with an estimated max over time.

Under the hood:

One room per couple

A Cloudflare Durable Object per couple over a hibernating WebSocket, named server-side from the token.

Offline first, for real

Writes queue in SQLite with client-assigned ids, fold as you type, and one pure function decides retry or give up.

Rest as an instant

A rest is sent as an end time, so one message covers it and the Live Activity counts down alone.

Replay what you missed

A reconnect re-sends presence, and a pending consent request rides the socket upgrade.

Consent, not access

Editing a partner’s weights goes through a gate they can revoke. Mirroring syncs completion only.

A design system as code

One variable Nunito file, 2pt borders, 4pt lips, no shadows, and a press with zero animation.