A workout app that pairs Tinder-style exercise discovery with training logic grounded in exercise-science research. Built as an installable PWA — add it to your home screen and it runs fullscreen like a native app, no App Store required.
▶ Live demo — setswipe.vercel.app — tap Try the live demo, no signup needed. The demo account is preloaded with 10 weeks of training history so the charts, streak and PRs are populated on arrival.
Most workout trackers treat a muscle as one thing you either train or don't. That's not how muscles work, and it's why so many self-built routines quietly skip the same things.
This app models muscles at the level of individual heads, which unlocks three features a coarser data model can't express.
Each day in your split carries a checklist of the muscle heads it's responsible for. Build a push day out of bench, incline, overhead press and pushdowns and the app tells you:
No direct work for side delts and triceps long head.
That's correct, and it's the most common gap in self-built push days. Pressing drives the front delt; the side delt needs abduction (lateral raises). The triceps long head crosses the shoulder, so it's only meaningfully loaded overhead — pushdowns bias the lateral head.
Crucially, a muscle worked only as a secondary counts as a gap, not a pass. Overhead pressing brushes the side delts; it is not side delt training.
Tap "Machine taken?" mid-workout and you get ranked alternatives scored by shared primary muscles, matching movement pattern, and your available equipment. The pec deck returns cable flyes and dumbbell flyes — not an unrelated press. A candidate sharing no primary muscle is never offered, because that isn't a substitute, it's a different exercise.
Swapping affects only that session's log. Your saved template is never mutated.
Every suggestion comes from your actual logged performance:
Last: 135×8 → try 140×8
Weight climbs only once you've cleared the top of the rep range on every target set; otherwise it holds and chases one more rep. PRs are detected at log time (by weight and by single-set volume). Plateaus surface only in the end-of-workout summary — never mid-set, where it would just be discouraging.
The muscle tagging isn't decorative. A few of the distinctions it encodes:
| Distinction | Why it matters |
|---|---|
| Front / side / rear delt | Lateral raises hit the side delt at ~66% MVC vs ~28% for overhead press |
| Triceps long vs lateral head | Overhead extensions produced ~50% more long-head growth than pushdowns |
| Biceps long vs short head | Long head is lengthened by shoulder extension, short head by flexion |
| Upper vs mid chest | Upper-pec activation peaks near a 30° incline and falls past 45° |
| Quads vs rectus femoris | Squats grow the vastii but barely the rectus femoris, which crosses the hip |
| Hamstrings hip vs knee | Hinges grow the long head; leg curls grow the short head |
| Gastrocnemius vs soleus | Knee straight vs knee bent selects which one you train |
One caveat the app deliberately respects: EMG activation is not hypertrophy. Hip thrusts out-activate squats at every glute site, yet a controlled trial found equivalent glute growth. Where activation and growth data disagree, the tagging follows growth.
Full writeup with sources: docs/EXERCISE_SCIENCE.md
The Coach tab combines two things: a weekly digest written by a scheduled AWS Lambda, and a short review from the Gemini API.
Everything factual in the model's prompt — total volume, which lifts have stalled, which muscles the routine never trains, recent PRs — is computed by the app. The model only phrases it, so the advice can't drift into invented training history. The page also lists exactly which figures the review was based on.
The prompt forbids restating the data, praise without instruction, and vague verbs like "optimise". An early version produced "continue completing 12 sessions"; it now produces "drop the overhead press to 90% and rebuild over three sessions, and move it to the front of the day while your shoulders are fresh."
Photos upload straight from the browser to a private S3 bucket via a one-minute
presigned PUT, so images never pass through the app server. Viewing uses a
separate five-minute presigned GET. Only the object key is stored in Postgres,
the bucket denies all public access, and content type and length are validated
before signing — a signature is a capability, so it shouldn't be minted for
something that would be rejected afterwards.
- Next.js 16 — App Router, Server Components, Server Actions
- TypeScript throughout
- Supabase — Postgres + Auth, row-level security on every table
- AWS — S3 for photo storage, Lambda on an EventBridge schedule for the weekly digest
- Google Gemini API — coaching review, grounded in app-computed facts
- Tailwind CSS v4 — CSS-first config with custom design tokens
- Vitest — 74 unit tests over the training logic
- Vercel hosting, GitHub Actions keep-alive
Supabase pauses free-tier projects after 7 days without database activity, which would take the whole deployment down. A scheduled GitHub Action reads one row every three days so the live demo doesn't quietly die.
Template and Log are strictly separate. The template is the plan; a log records what actually happened. They can diverge — different weight, a swapped exercise, a skipped set — without corrupting the plan. That separation is what makes mid-workout swaps safe.
Equipment tiers are cumulative, not exclusive. bodyweight < home < full_gym. An
exercise shows when its required tier is at or below yours, so bodyweight moves appear for
everyone. Filtering uses a rank comparison, never an exact match.
Coverage requirements adapt to equipment. A bodyweight-only user is never told they're
missing side-delt isolation that doesn't exist for them — requiredFor() narrows the
checklist to what the library can actually train at that tier.
Authorization is enforced in depth. The proxy redirect is UX only; the real guarantees are Postgres RLS policies plus an explicit ownership re-check inside every Server Action.
The exercise library is a single source of truth. src/lib/exercise-library.ts is
TypeScript; npm run seed:sql generates the SQL seed from it, so app and database can't
drift.
| Concern | File |
|---|---|
| Muscle taxonomy + the science behind it | src/lib/muscles.ts |
| 201 tagged exercises (source of truth) | src/lib/exercise-library.ts |
| Split recommendation, day requirements | src/lib/splits.ts |
| Overload, PR detection, plateau detection | src/lib/overload.ts |
| Substitution ranking | src/lib/substitutions.ts |
| Coverage grading | src/lib/coverage.ts |
| Streaks, heatmap bucketing | src/lib/stats.ts |
| Gemini client and prompt | src/lib/gemini.ts |
| S3 presigning | src/lib/s3.ts |
| Weekly digest Lambda | aws/digest-lambda/index.mjs |
| AWS provisioning | aws/setup.sh |
Next.js 16 renamed Middleware to Proxy — the root file is
proxy.ts, notmiddleware.ts. Amiddleware.tsin this version silently never runs.
git clone https://github.com/KaushikApp123/setswipe.git
cd setswipe
npm install
cp .env.example .env.local # add your Supabase URL + anon keyThen in the Supabase SQL Editor, run in order:
supabase/schema.sql— tables + RLS policiessupabase/migrations/002_app_features.sql— onboarding fields, PR flagssupabase/migrations/003_exercise_science.sql— muscle-head metadatasupabase/migrations/004_digests_and_photos.sql— weekly digests, photossupabase/seed_exercises.sql— 201 tagged exercises (generated, re-runnable)
npm run dev # http://localhost:3000
npm test # 74 unit tests
npm run seed:sql # regenerate the exercise seed from TypeScriptSet DEMO_EMAIL / DEMO_PASSWORD in .env.local, create that account in the app, then:
npm run seed:demoThis writes ~10 weeks of realistic history — progressive overload, PRs, and one deliberate plateau — through the demo user's own session, so no service-role key is needed. Re-run any time to reset the demo after visitors have poked at it.
Both features degrade gracefully — the app runs fine without either, showing an explanatory empty state instead.
bash aws/setup.sh # S3 bucket, scoped IAM user, Lambda, EventBridge scheduleThen set the function's own secrets (the service-role key bypasses RLS, so it belongs only on the Lambda — never in the app or this repo):
aws lambda update-function-configuration \
--function-name setswipe-weekly-digest \
--environment "Variables={SUPABASE_URL=...,SUPABASE_SERVICE_ROLE_KEY=...}"Add GEMINI_API_KEY (free tier, aistudio.google.com/apikey)
plus AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and
S3_PHOTOS_BUCKET to .env.local.
- Nutrition tracking — no reliable free food API since MyFitnessPal's closed; it's a separate product
- Per-gym equipment inventory — replaced by three fixed equipment tiers
- Native Lock Screen / Live Activities — iOS-native only (ActivityKit), an accepted trade-off of shipping as a PWA
MIT