Skip to content
DotimsPublic

About

Offline-first workout tracker: Expo/React Native app with SQLite sync, a GraphQL social feed, a web dashboard and an MCP coach.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

GripLog logo

GripLog

An offline-first workout tracker for iOS and Android, with a sync API, a social feed, a web dashboard and an AI coach that reads your real training data.

CI Expo SDK 57 React Native 0.86 TypeScript strict License: MIT

GripLog on Android with a real training history: the weekly goal on the feed, the profile cover made from training days with a lift goal ring, a workout with the muscles it trained, and logging a routine through to its summary

GripLog is the app I actually log my gym sessions in. It started as a Hevy replacement and grew into a full stack project: a React Native app that works with zero signal on the gym floor, a Postgres backed API that syncs it, a GraphQL feed for training with friends, a web dashboard for progress charts, and an MCP server that lets an assistant like Claude review your training using numbers computed by tested code instead of guesses.

My own history (144 workouts and 2 600+ sets exported from Hevy) imports in about a second, and the recording and screenshots show that real data: the week against my goal, a profile cover drawn from my training days with a ring for my lift goal, a past workout with the muscles it trained, and a session logged from a routine through to its summary. Both were captured from the app running in Expo Go on an Android emulator.

The interface is dark and quiet on purpose: you look at it between sets, often in a dim gym. Colour carries meaning only: red for actions and progress, green for completed sets, gold for personal records, and one colour per superset. Type is Onest in four weights with tabular figures, so timers and weights never jitter. Motion is Reanimated on the UI thread: spring press feedback, a sliding segmented control, sheets that rise and settle, a set that washes green when ticked, a rest line that drains continuously and a summary that counts its numbers up.

Small rewards keep it pleasant to come back to, the way Duolingo does it: a weekly goal with a streak, a short chime when a workout is saved (a brighter fanfare for a personal record), a soft tick for each set and a bell when rest is over. The sounds are generated for the app, mix with your music and stay quiet when the phone is on silent.

Profile with the training map cover and lift goal ring, exercise detail with a next-session suggestion, an active workout with a superset and the rest timer, and a past workout with its muscle map

Features

In the gym (fully offline)

  • Log sets with weight, reps and RPE, as normal, warm-up, drop or failure sets. Exercises without a load take reps only, timed ones (plank, carries) take a stopwatch-style time, cardio takes distance and time. A short swipe on a set reveals delete; a long one deletes it straight away.
  • The "previous" column shows what you lifted last time; ticking an empty set fills it in.
  • Rest timer that survives the app being killed and buzzes the locked phone with a local notification.
  • 400+ exercises, each with an animated 3D demo of the movement with the working muscles highlighted, plus search, muscle filters, your own custom exercises and the lifts you have logged before. The library is sorted by how often you do each lift.
  • Routines, built on their own page, with targets per set: a fixed weight and reps, a rep range like 8-12, or a time. A range is the hint you aim for while logging. A new exercise starts with one set; more are a tap away.
  • Supersets: link neighbouring exercises in a routine or mid-workout; the rest timer waits until the last exercise of each round.
  • "Repeat this workout" and "save as routine".
  • Personal records (heaviest weight, estimated 1RM, best set volume, most reps) recomputed on every save, edit and delete.
  • A weekly goal (say three workouts) with a streak of weeks on goal. Every four weeks on goal earn a freeze that covers one missed week, for a cold or a holiday.
  • A lift goal, like a 100 kg bench: a ring shows your best recent estimated 1RM against the target, and a trend line over the last twelve weeks forecasts when you will get there.
  • A finish screen worth seeing: the week so far, a body map of the muscles you trained and a photo of the day.
  • Photos on workouts, from the camera or the library. They stay on the phone and build your profile cover; without photos the cover is a map of the days you trained.
  • Every workout shows the muscles it trained on a front and back body map, and your recent workouts use it as their thumbnail.
  • A next-session suggestion per lift: double progression, plus a deload after three sessions without progress.
  • One-tap import of a Hevy CSV export, with a review step and safe re-runs.

With an account

  • Background sync across devices. The phone stays the source of truth while you train.
  • Find friends by name or from suggestions (the most active people you do not follow yet), follow them, see their workouts in the Friends feed and like them.
  • A web dashboard with the feed and progress charts for every exercise.
  • An MCP server, so Claude (or any MCP client) can answer "how is my bench going?" from your actual history.

Architecture

flowchart LR
  subgraph Phone["Expo app (iOS / Android)"]
    UI[Screens] --> Z[Zustand<br/>active workout]
    UI --> Q[TanStack Query<br/>server state]
    UI --> L[Live queries]
    L --> DB[(SQLite + Drizzle)]
    DB --> OB[Outbox]
  end
  subgraph Server["API (Hono, Node)"]
    REST["REST /sync"]
    GQL["GraphQL /graphql<br/>Yoga + Pothos"]
    AUTH[Better Auth]
    PG[(PostgreSQL + Drizzle)]
    REST --> PG
    GQL --> PG
  end
  OB -- "push / pull" --> REST
  Q -- feed, likes --> GQL
  WEB["Web dashboard<br/>Vite + TanStack Router"] --> GQL
  MCP["MCP coach"] -- bearer token --> GQL
  CORE["@griplog/core<br/>training rules, Hevy import,<br/>sync contract (zod)"]
  CORE -.-> Phone
  CORE -.-> Server
  CORE -.-> WEB
  CORE -.-> MCP
Loading

The training logic lives in one pure TypeScript package, @griplog/core, used by the app, the API, the web dashboard and the MCP server. A 1RM estimate or a progression rule is written and tested once, and every surface shows the same number.

Engineering notes

Things I deliberately built the hard way, because they are the interesting part.

Transactions, end to end

  • Saving a workout is one SQLite transaction: the workout row, its exercises and sets, the recomputed personal records and the sync outbox entry all land together or not at all. There is a test that breaks a foreign key halfway through and checks that nothing was written.
  • Transactional outbox: every local write queues its aggregate in the same transaction, so a change can never be saved without also being queued for upload.
  • Pulled pages and the sync cursor commit together, so a crash mid-sync re-fetches the same page instead of skipping it.
  • On the server, a push is one Postgres transaction under a per-user advisory lock, so version numbers follow commit order and incremental pulls never miss a row.

Sync that survives real networks

  • Idempotency keys: each change has an id the server records. A phone that lost the response and retries gets "skipped", not a duplicate.
  • Last write wins on updatedAt, and a guessed id can never overwrite another user's data (tested).
  • Poison messages are parked: the client validates every payload with the same zod schema as the server. A bad change (my Hevy export contains a 2 505 kg curl) is set aside with a reason instead of blocking the queue, and a rejected batch is bisected to find the culprit.
  • Pulls page per entity type with a cursor that may hold back but never skips a row.
  • The contract evolves without breaking old phones: new fields (superset numbers, rep ranges, timed targets) have defaults in the zod schema, so an app version that does not know them still pushes valid changes.

REST and GraphQL, each where it wins

  • REST for sync: two fixed commands (POST /sync/push, GET /sync/pull) with plain status codes and validation.
  • GraphQL for the social feed and dashboards: clients ask for exactly the fields a card shows, in one round trip. Per-request DataLoaders batch authors, sets and likes to avoid N+1 queries, and the feed uses keyset pagination on (startedAt, id) instead of OFFSET.

State, by kind of data

State Tool Why
The running workout Zustand, persisted to SQLite Changes on every keystroke; components subscribe to the slice they show, so typing a weight re-renders one set row.
Database handle, session, preferences React Context Global, rarely changes, read everywhere.
Local tables useSyncExternalStore live queries First render already has data; one re-read per transaction, not per row.
Server data (feed, likes, search) TanStack Query Caching, infinite pages, optimistic likes with rollback.

Tests and CI

  • Vitest for the core rules, the Hevy importer and the API. API tests run against a real Postgres (a service container in CI) and cover idempotency, last-write-wins, ownership, rollback and pagination.
  • Jest + React Native Testing Library for repositories (real migrations on SQLite compiled to WebAssembly), the Zustand stores, the sync engine and components.
  • Maestro flows in apps/mobile/e2e for logging a workout and creating a routine.
  • GitHub Actions runs format, lint, typecheck and every test suite on each push and pull request.

Tech stack

Area Stack
Mobile Expo SDK 57, React Native 0.86, React 19.2 with the React Compiler, Expo Router (typed routes), Reanimated 4, Gesture Handler, react-native-svg
Local data expo-sqlite, Drizzle ORM and migrations, expo-secure-store
State Zustand, React Context, TanStack Query
API Hono, Better Auth, Drizzle on PostgreSQL 17, zod, GraphQL Yoga, Pothos, DataLoader
Web Vite, React 19, TanStack Router, TanStack Query
AI coach Model Context Protocol TypeScript SDK
Quality TypeScript strict, ESLint, Prettier, Vitest, Jest, React Native Testing Library, Maestro, GitHub Actions

Repository layout

apps/
  mobile/      Expo app: screens, SQLite data layer, sync engine
  api/         Hono server: auth, REST sync, GraphQL
  web/         Vite dashboard
  mcp/         MCP server for AI assistants
packages/
  core/        Training rules, Hevy importer, sync contract (zod)
  catalogue/   Exercise catalogue shared by the app and the API

Getting started

You need Node 22+ and Docker.

npm install
docker compose up -d                      # Postgres on localhost:5440
cp apps/api/.env.example apps/api/.env    # set BETTER_AUTH_SECRET
npm run dev -w @griplog/api               # API on http://localhost:3000
npm run db:seed-demo -w @griplog/api      # optional: three demo friends with recent workouts

Mobile. Install Expo Go (SDK 57) on your phone, then:

cp apps/mobile/.env.example apps/mobile/.env   # EXPO_PUBLIC_API_URL=http://<your LAN IP>:3000
npm run mobile                                  # scan the QR code with the camera

The app works without the API; an account only adds sync and the social feed. To bring your history over, export workouts from Hevy (Settings, Export & Import Data) and open Import from Hevy.

Web dashboard.

npm run dev -w @griplog/web     # http://localhost:5173, proxies /api and /graphql to the API

Web dashboard: estimated 1RM chart and next-session suggestion

AI coach (MCP). Create a token and register the server with your MCP client, for example Claude Code (run from the repository root):

npm run token -w @griplog/mcp -- you@example.com your-password
claude mcp add griplog -e GRIPLOG_TOKEN=<token> -e GRIPLOG_API_URL=http://localhost:3000 \
  -- npx tsx apps/mcp/src/index.ts

Then ask "review my last four weeks" or use the review_my_training prompt. Tools: list_exercises, exercise_progress, recent_workouts, get_workout, training_overview.

Checks

npm run format:check
npm run lint
npm run typecheck
npm test            # API tests expect the Postgres from docker compose

Roadmap

  • Offline app: library, routines, workouts, records, history, profile, Hevy import
  • Polish: animations, haptics, charts, rest notifications
  • API, accounts and sync
  • GraphQL social feed
  • Web dashboard
  • MCP coach
  • Supersets, rep ranges and timed targets in routines
  • Weekly goal with streak freezes, a lift goal with a forecast, workout photos, sounds
  • iOS Live Activity for the rest timer and EAS builds
  • Hosted API and web deploy
  • In-app coach chat on top of the MCP tools

Credits

  • Exercise data and photos: free-exercise-db (public domain).
  • Exercise animations: ExerciseDB, © Gym visual. They are streamed from the ExerciseDB CDN and not stored in this repository; packages/catalogue/data/animations.json holds only the matched ids, built by npm run animations -w @griplog/catalogue.
  • Font: Onest (SIL Open Font License).
  • Body map: react-native-body-highlighter (MIT).

License

MIT © Radosław Marek

About

Offline-first workout tracker: Expo/React Native app with SQLite sync, a GraphQL social feed, a web dashboard and an MCP coach.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages