Projects

FITNESS

A full-stack training app with a React Native client that keeps working offline after sign-in, and a Node.js, Express and PostgreSQL API.

React Native, Expo, JavaScript, Node.js, Express, Sequelize, PostgreSQL, JWT, OAuth, OpenAI API

TL;DR

What it does

Architecture

React Native (Expo) client                 Node.js / Express API
┌────────────────────────────┐            ┌──────────────────────────────┐
│ Screens                    │  REST +    │ /users  /auth  /ai           │
│ UserContext (state + sync) │ ─ JWT ───► │ JWT middleware, rate limits  │
│ AsyncStorage + SecureStore │            │ Sequelize models             │
└────────────────────────────┘            └──────────────┬───────────────┘
                                                         ▼
                                     PostgreSQL · Cloudinary · OpenAI

Code choices and why

Working offline after sign-in

A workout app gets used in gyms, garages and basements, so it can’t depend on signal for every action. It does need a connection once: signing in is what gives the device a login token and a cached copy of the user’s profile, both kept in expo-secure-store. On later launches, AuthGate restores the user from that cache right away, so the app opens without signal and refreshes the profile in the background when it can.

After that, completed workouts and food logs go through UserContext the same way:

  1. Update local state and AsyncStorage first, so the UI responds instantly.
  2. If the request fails or the device is offline, push the action onto a persisted pendingOps queue.
  3. A NetInfo listener replays the queue when connectivity returns. Anything that still fails stays queued for the next try.

Once signed in, the user never loses a completed workout or food log because of a dead zone.

The limits, straight from the code:

A calendar row per day, with JSONB

The server stores one Calendar row per user per day, with workouts and foods as JSONB arrays. Logging is always “what happened on this day,” so the data is shaped the way it’s read. Workouts are deduplicated by an external ID, so a replayed offline write can’t create a duplicate.

Security that’s proportionate

AI that fails gracefully

Daily workouts come from the OpenAI API with a strict contract: return a single JSON object, exactly four exercises (upper push, upper pull, lower body, and a core or conditioning finisher), with realistic volumes. The server validates the response and wraps the call in a 15-second timeout. If the AI isn’t configured, times out or returns bad JSON, the API responds with a clear 503 and an error code instead of a broken workout.

Images handled on the server

Profile photos are uploaded in memory with a 5 MB limit, resized to 512×512 WebP with sharp, and stored on Cloudinary. The phone never has to do the image work.

What’s next

Consolidating the API URL handling into a single module, using one consistent key for the stored auth token, and adding a consistency and streak view.