A two-sided coaching SaaS for personal trainers and their clients.
Trainers build workout programs, assign them to clients, attach demo videos, and track engagement. Clients get their own portal to watch demos, log their own sessions, see their progress, and message their coach. Built as a full-stack MERN-style app on Next.js 16 + MongoDB, with motion-rich UI throughout.
This is a portfolio project demonstrating real-product patterns: role-based two-sided auth, an invite flow, drag-and-drop builders, file/video uploads, engagement analytics, and in-app notifications.
- Animated landing page β parallax hero, 3D-tilt feature cards, scroll-triggered count-up stats, animated pricing toggle, buttery smooth scrolling (Lenis).
- Email/password auth (Auth.js v5 / NextAuth) with JWT sessions, bcrypt hashing.
- Optional Google OAuth (auto-enabled when credentials are provided).
- Two roles β
trainerandclientβ with role-based routing:/dashboardfor trainers,/appfor clients. The wrong role is redirected. - Invite flow β a trainer adds a client, copies an invite link, and the client sets a password to create their own linked account.
- Clients β full CRUD with an animated list, search, status (active/paused/ archived), goals, and per-client invite links.
- Workout builder β drag-and-drop exercise ordering (dnd-kit) with per-exercise sets / reps / rest, plus an exercise picker filtered by muscle group.
- Demo videos β attach a Cloudinary-hosted video to any exercise.
- Assignments β assign workouts from the library to clients.
- Progress β log sessions on a client's behalf; view an animated volume chart (Recharts) + session history.
- Engagement β per-client video watch-% and exercise completion at a glance.
- Messaging β a two-way chat thread with each client.
- Notifications β an in-app bell with unread counts.
- Settings β profile, business name, units (kg/lb), avatar upload, password.
- Today β assigned workouts + quick stats.
- Workout view β watch each exercise's demo video (watch progress tracked automatically) and log your own sets/reps/weight.
- Progress β personal volume chart + full session history.
- Messages β chat with your coach.
- Settings β profile, units, avatar, password.
- Mobile-friendly bottom navigation.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, React 19) |
| Language | TypeScript |
| Database | MongoDB + Mongoose |
| Auth | Auth.js v5 (NextAuth) β Credentials + optional Google |
| Validation | Zod (shared client/server schemas) |
| Styling | Tailwind CSS v4 |
| Animation | Framer Motion (components) Β· Lenis (smooth scroll) |
| Drag & drop | dnd-kit |
| Charts | Recharts |
| Media | Cloudinary (unsigned uploads β avatars & exercise videos) |
| Icons | lucide-react |
Both roles share one sign-in page and one User collection. The role field
(trainer | client) drives everything:
| Role | Home | Capabilities |
|---|---|---|
| Trainer | /dashboard |
Build workouts, manage clients, assign, log on behalf, review, chat |
| Client | /app |
Watch demos, log own sessions, view progress, chat with coach |
Routing is enforced in two places:
src/proxy.ts(Next.js 16's renamed middleware) +src/auth.config.tsβ an edge-safeauthorizedcallback gates/dashboard(trainer-only) and/app(client-only), and redirects logged-in users away from the auth pages to their role's home.- Server components additionally call
requireTrainer()/requireClient()fromsrc/lib/auth-helpers.tsas a defense-in-depth check.
Auth.js config is split so middleware stays edge-safe:
auth.config.tsholds only edge-compatible logic (callbacks, routing), whileauth.tsadds the Credentials/Google providers and Mongoose access used in the Node runtime.
Trainer adds client ββΆ server generates inviteToken ββΆ trainer copies /invite/<token>
β β
βΌ βΌ
Client record (inviteStatus: pending) Client opens link, sets password
β β
βββββββββββββββββ linked via Client.user βββ User created (role: client)
inviteStatus: accepted
Tokens are generated on demand (POST /api/clients/[id]/invite), which also
backfills clients created before the field existed.
- Server Components read directly from MongoDB via Mongoose and pass plain serialized objects to client components.
- Client Components mutate through Route Handlers (
/api/**), which validate input with shared Zod schemas and enforce ownership. - Notifications are written as a best-effort side effect of key actions (assign, log, message, invite-accepted) and polled by the bell component.
- Every
/apiroute checks the session and that the resource belongs to the caller (a trainer only touches their own clients/workouts; a client only their own data, resolved viaClient.user). - Passwords hashed with bcrypt (cost 12);
passwordHashisselect: false. - Sign-in, registration, and invite acceptance use a MongoDB-backed rate limiter shared across serverless instances. Limiter keys are SHA-256 hashes, not raw email or IP values.
- New passwords require 10+ characters with uppercase, lowercase, and a number.
- Google OAuth identities are resolved to the same MongoDB user IDs used by all protected routes.
- Deletes cascade: removing a client also removes their assignments, sessions, messages, notifications, and linked login β nothing is left orphaned.
| Model | Purpose |
|---|---|
User |
Trainer or client account (role, bio, units, businessName, image) |
Client |
A trainer's client record; optionally linked to a User via invite |
Workout |
A program; embeds blocks (exercise + sets/reps/rest + videoUrl) |
WorkoutAssignment |
Junction: which workout is assigned to which client |
WorkoutSession |
A logged session (per-set reps Γ weight); powers progress + volume |
Message |
One chat message in a trainerβclient thread |
Notification |
An in-app notification for a user |
VideoProgress |
Per (client, workout, exercise) watch progress: %, completed |
RateLimit |
Expiring counters for public authentication endpoints |
Exercises themselves are a static catalog in src/lib/exercises.ts (muscle group
- equipment), keeping the demo self-contained.
src/
βββ app/
β βββ (auth)/ # Sign-in / sign-up (shared animated layout)
β βββ api/ # Route Handlers
β β βββ auth/[...nextauth]/ # Auth.js handlers
β β βββ register/ # Trainer sign-up
β β βββ invite/[token]/ # Validate + accept invite
β β βββ clients/[id]/... # Clients + nested assignments/sessions/messages/invite
β β βββ workouts/[id]/ # Workout CRUD
β β βββ me/... # Client-facing: workouts, sessions, messages, video-progress, profile, password
β β βββ notifications/ # List + mark-read
β βββ dashboard/ # Trainer area (sidebar + topbar + mobile nav)
β β βββ clients/[id]/ # Tabbed detail: Overview/Workouts/Progress/Engagement/Messages
β β βββ workouts/[new|[id]] # Builder + detail
β β βββ settings/
β βββ app/ # Client portal (own nav)
β β βββ workouts/[id]/ # Watch videos + log session
β β βββ progress/ messages/ settings/
β βββ invite/[token]/ # Public invite-accept page
β βββ layout.tsx # Root: fonts, SessionProvider, Lenis
β βββ page.tsx # Landing page
βββ components/
β βββ sections/ # Landing page sections
β βββ dashboard/ client-portal/ client-detail/ workouts/ messaging/ notifications/ settings/
β βββ SmoothScrollProvider.tsx AuthSessionProvider.tsx Navbar.tsx
βββ lib/
β βββ mongoose.ts mongodb.ts # Cached connections
β βββ auth-helpers.ts api-helpers.ts
β βββ exercises.ts units.ts utils.ts
β βββ security.ts # Password/email policy + hashed limiter keys
β βββ rate-limit.ts # MongoDB-backed fixed-window rate limiting
β βββ schemas/ # Zod: client, workout, progress, video, profile
βββ models/ # Mongoose models (see table above)
βββ auth.ts auth.config.ts # Auth.js (split for edge safety)
βββ proxy.ts # Next 16 middleware β role-based routing
- Node.js 20.9+
- A MongoDB database (local or MongoDB Atlas β free tier is plenty)
git clone https://github.com/farhan-shafi/reppod.git
cd reppod
npm install
cp .env.example .env.local # then fill in the values below
npm run dev # http://localhost:3000| Variable | Required | Notes |
|---|---|---|
AUTH_SECRET |
β | openssl rand -base64 32 |
MONGODB_URI |
β | Atlas string or mongodb://localhost:27017/reppod |
GOOGLE_CLIENT_ID / ..._SECRET |
β | Enables Google sign-in when both are set |
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME |
β | Enables avatar + exercise-video uploads |
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET |
β | Unsigned preset restricted as described below |
NEXT_PUBLIC_*vars are read at server start β restartnpm run devafter changing them.
For Cloudinary, the upload preset is the security boundary because browser-side
checks can be bypassed. Restrict the preset in Cloudinary to the reppod folder,
JPG/PNG/WebP/AVIF images and MP4/WebM/MOV videos, with maximum sizes of 10 MB for
images and 50 MB for videos. Do not reuse that unsigned preset for other apps.
npm run dev # start dev server (Turbopack)
npm test # security policy unit tests
npm run lint # ESLint
npm run build # production build
npm run start # serve the production build- Sign up at
/sign-upβ you're a trainer in/dashboard. - Build a workout (Workouts β New) β drag in exercises, set reps, optionally attach a demo video.
- Add a client β copy their invite link.
- Open the invite link in an incognito window β set a password β you're now in
the client portal (
/app). - As the client: open the workout, watch the video, log your sets.
- Back as the trainer: open the client β Engagement shows watch %, Progress shows the logged session, and Messages works both ways.
- Animated marketing site
- Two-sided auth + invite flow + role routing
- Clients CRUD, workout builder, assignments
- Progress logging + charts
- Two-way messaging + notifications
- Settings, profile, Cloudinary avatars
- Per-exercise demo videos + watch tracking + engagement analytics
- Billing (LemonSqueezy β Pakistan/global-friendly merchant of record)
- Password reset via email
- Streak / achievement animations
- Built on Next.js 16 β Turbopack is the default; middleware is
proxy.ts. - All interactive UI is client components; everything else is server-rendered for speed and SEO.
- Demo data is self-contained β no external services are required to run the core app (Cloudinary and Google are optional enhancements).
MIT Β© 2026 Farhan Shafi
