Loading

All projects

Mobile App · 2024

Pulse Fitness App

Users churned in week one because every tap needed a connection — useless in a basement gym. Pulse was rebuilt offline-first: log a full session on airplane mode, sync when you're back.

React NativeSupabase

Client

Pulse Fitness

Industry

Health & Fitness

Timeline

10 weeks

My role

Mobile Developer

The problem

Users dropped off after week one because the app needed a connection for every single action.

The solution

Offline-first workout tracking with sync, streaks and smart push reminders that bring people back on rest days.

+58%

Week-4 retention

4.8★

Store rating

12k

Active users

What I built

Offline-first logging

Local SQLite is the source of truth; the server is a sync target.

Streaks & rest days

Streak logic that survives planned rest days instead of punishing them.

Smart reminders

Push notifications scheduled from the user's real training pattern.

Progress graphs

Volume, 1RM estimates and body metrics over any time range.

Architecture

  • React Native (Expo) with a typed local SQLite layer
  • Supabase Postgres + Auth as the sync backend
  • Delta sync protocol with per-row updated_at and tombstones
  • Expo Notifications for scheduled reminders, EAS for OTA updates

Hard problems

Conflicting edits

Last-write-wins per field with a vector clock on the set level — no more lost reps.

Battery drain

Sync batched into a single WorkManager job every 15 min instead of a live socket.

Code from the build

A few real excerpts from the repository.

src/sync/push.ts
export async function pushChanges() {
  const dirty = await local.query<Row>(
    "select * from sets where dirty = 1 order by updated_at asc limit 500"
  );
  if (!dirty.length) return;

  const { error } = await supabase.from("sets").upsert(
    dirty.map(stripLocalFields),
    { onConflict: "id" }
  );
  if (error) return scheduleRetry(error);

  await local.exec("update sets set dirty = 0 where id in (?)", [dirty.map(d => d.id)]);
}
src/logic/streak.ts
export function streak(days: Date[], restDays: number[] = [0]) {
  let count = 0;
  for (const day of eachDayBack(new Date())) {
    if (restDays.includes(day.getDay())) continue;  // planned rest never breaks it
    if (!days.some(d => isSameDay(d, day))) break;
    count++;
  }
  return count;
}
"Week-4 retention went from 22% to 35%. The offline rewrite paid for itself in one month."
Daniel P. · CTO, Pulse

How it was built

  • Discovery call, scope and fixed timeline agreed up front
  • Custom design system built in Figma before development
  • Typed, reviewed codebase with CI checks on every commit
  • Deployed with monitoring, backups and post-launch support

Want something like this?

Start a Project