Case 04Gym management
GymOS
Enrollment, billing and check-in for two gyms in a single system.
A management system I designed and built on my own for a neighborhood gym with two locations: member records, plans, recurring billing, check-in, classes, workouts and KPIs in one place. The front desk stops checking notebooks and spreadsheets, and late payments are handled the day they happen.
- Client
- Neighborhood gym in São José dos Campos, Brazil · 2 locations
- Period
- 2026
- Team
- Solo project · from discovery to rollout
- Role
- Full stack engineer · custom build, end to end
- Stack
- Next.js · React · TypeScript · Tailwind CSS · NestJS · Prisma
- Confidentiality
- Client data kept private
Results in numbers
- 500+active members managedtwo locations and 30 instructors in one system
- 40%fewer late payments in six monthsrecurring billing with a retry schedule
- 3 sto clear a member at the entrancepreviously a manual form and payment check
- 12 hper week given back to the front deskno billing spreadsheet, no entry notebook
The business context
- Client
- Neighborhood gym in São José dos Campos, Brazil · 2 locations
- Period
- 2026
The gym serves more than 500 members across two locations in the same neighborhood, with 30 instructors rotating shifts and classes. Until then, operations ran on paper forms, one spreadsheet per location and fees collected at the front desk. Staff checked the member’s name in a notebook, nobody knew for sure who was up to date, who could train at the other location or when a plan expired, and late payments only surfaced at month-end close, when they were already hard to recover.
The challenge
The owner did not want generic off-the-shelf software: he wanted the system to follow the house rules. That meant controlling access by plan and by location, charging every month without depending on someone’s memory, blocking and releasing members according to payment and giving instructors their schedule and student list without exposing the finances. All of it for a team that had never used any system before.
Two locations, one operation
Each location has its own hours, capacity and staff, but the member is one. Some plans are valid at one location and others at both, and each location’s manager should only see their own.
Late payments found too late
Without recurring billing, the monthly fee depended on the member remembering and the front desk writing it down. Members who fell behind kept training for weeks until someone noticed in the spreadsheet.
The front desk as a bottleneck
At peak hours a line formed at the entrance: find the form, check the payment, ask about the plan. Manual checks slowed members down and still let in people who should not get in.
What I delivered
I built GymOS as a monorepo with a NestJS API, Next.js dashboards and PostgreSQL through Prisma. Each person has their own entry point: front desk and management in the gym dashboard, instructors in the instructor portal, members in their own area with check-in, classes, workouts and progress. Behind it, a single access rule decides who gets into each location, and a recurring billing flow charges, retries, blocks and releases on its own.
Recurring billing with dunning
Monthly fee generated on the due date, up to 3 attempts at 0, 3 and 7 days and automatic blocking of unpaid members.
Check-in with a single rule
Status, plan, freeze, overdue bills, location and weekly limit checked in one call, from the member app or the front desk.
Instructor portal and member area
Instructors get schedule, classes and attendance; members get workouts, class bookings and body-measurement progress.
Management dashboard and BI
Recurring revenue, churn, average ticket, occupancy by hour, finance, point of sale and stock for both locations.
How it works
Follow the work through the system step by step, or click any component on the board to see what it does.
drag to see the flow
Legend
- Synchronous
- Asynchronous
- Secure channel
- External integration
Text description of the flow
- From Front desk and management to GymOS API — Enrollment (synchronous).
- From Member area to GymOS API — Check-in and bookings (synchronous).
- From Instructor portal to GymOS API — Attendance (synchronous).
- From GymOS API to Access control — Validates entry (synchronous).
- From GymOS API to Recurring billing — Activates the plan (synchronous).
- From GymOS API to Classes and workouts — Bookings (synchronous).
- From Access control to Gym database — Records check-in (synchronous).
- From Recurring billing to Gym database — Subscription (synchronous).
- From Recurring billing to Queues and real time — Queues notice (asynchronous).
- From Recurring billing to Payment gateway — Charges monthly fee (external integration).
- From Classes and workouts to Gym database — Workouts (synchronous).
- From Queues and real time to Email — Retries (asynchronous).
Walk through the flow
←→change step
5 steps
From start to finish
Pick a step or press Play to follow the whole flow. The board highlights where the work goes as you move along.
System components
Complete member record
- What it solves
- Replace the paper form with a record that follows the member from day one to renewal.
- How it works
- Enrollment brings together tax ID, contact, emergency contact, an address filled in from the postal code via ViaCEP, the PAR-Q questionnaire and health notes. Physical assessments store weight, body fat and measurements; attachments store photo and contract. Every status change is kept in history, and the member gets a link to activate their own account.
NestJS · Prisma · PostgreSQL · ViaCEP
Plans and subscriptions
- What it solves
- Turn the gym’s plans into rules the system enforces on its own.
- How it works
- Each plan defines price, monthly, quarterly or yearly cycle, enrollment fee, weekly access limit and whether it covers one location or both. A subscription can be frozen, cancelled with a reason or expired, generates the member’s contract and feeds the sign-ups versus cancellations report.
NestJS · Prisma · Next.js
Recurring billing and dunning
- What it solves
- Charge every month without relying on anyone’s memory, and act the same day a payment fails.
- How it works
- The billing process finds subscriptions coming due, creates the payment once per due date and makes up to three attempts, at 0, 3 and 7 days. Every attempt and event is recorded. Once attempts run out, the subscription goes past due and the member is suspended; when payment comes in, the next due date is calculated and access returns immediately.
NestJS · Prisma · PostgreSQL
Payment gateway behind an adapter
- What it solves
- Receive payment confirmations securely and never process the same event twice.
- How it works
- The gateway sits behind a single interface, with a Stripe adapter and a test adapter. Every webhook has its HMAC SHA-256 signature checked with a constant-time comparison, and the event ID is unique in the database: if the gateway resends, the duplicate is dropped with no side effects.
NestJS · Stripe · HMAC
Access control at each location
- What it solves
- Decide in seconds, and always the same way, whether a member can enter that location.
- How it works
- A single validation checks member status, active subscription, freeze, overdue payments and invoices, the location allowed by the plan and the weekly access limit, and blocks duplicate check-ins on the same day. The same rule serves the member app, the front desk and an integration endpoint ready for a turnstile.
NestJS · Prisma · Next.js
Classes, workouts and instructor portal
- What it solves
- Organize the routine of 30 instructors and put workouts and schedule on the member’s phone.
- How it works
- Classes have a location, instructor, day, time and capacity; bookings respect available spots and move through booked, checked in, present, no-show or cancelled. Instructors see their own schedule and students and mark attendance. Workouts bundle exercises with sets, reps, rest and video, and members track their measurements in their own area.
Next.js · NestJS · Prisma
Decisions that hold the system up
One database, two locations
Instead of one system per location, every record carries the gym and the location, and each manager only sees their own. The member stays one person, the plan decides where they train and the owner sees both locations together. Opening a third location becomes a form, not a project.
The access rule lives in one place
Member app, front desk and turnstile all call the same validation. There is no "it works at the desk but not in the app": any rule change applies to every entry point at once.
Late payments handled by the schedule, not by people
I chose three attempts spaced at 0, 3 and 7 days, with automatic suspension at the end and automatic reactivation on payment. The front desk no longer has the awkward conversation at the counter, and members know exactly what happens if they fall behind.
Gateway behind an adapter
Billing talks to an interface, not to a vendor. Switching gateways or testing the whole flow without charging anyone is a matter of configuration, and signed, idempotent webhooks protect the books from resends and forged requests.
Business results
Late payments are no longer found at month-end: members who do not pay are notified, charged again at 3 and 7 days and blocked at the entrance until they settle.
Front-desk check-in dropped from about 1 minute to a few seconds, and the peak-hour line disappeared.
The owner follows recurring revenue, churn and occupancy for both locations in real time, without building spreadsheets.
All 30 instructors now see their schedule, classes and students in their own portal, and shift swaps left the group chat.
My role
- Role
- Full stack engineer · custom build, end to end
- Team
- Solo project · from discovery to rollout
I built GymOS on my own as a custom project for the client. I started by sitting down with the owner and the front desk to understand how the gym really worked: plans, exceptions, peak hours, what slowed the day down. From there I designed the architecture, modeled the data, built the API, the dashboards and the member area, put it in production with Docker, trained the staff at both locations and keep supporting and evolving the system with them.
What I owned
- Ran discovery with the owner and the front desk and turned the house rules into requirements.
- Modeled the domain in PostgreSQL with Prisma: members, plans, subscriptions, payments, invoices, check-ins, classes and workouts.
- Built the modular NestJS API with JWT, five access roles and per-location scoping.
- Implemented recurring billing, the dunning flow and the gateway’s signed webhooks.
- Developed the Next.js dashboards for management, instructors and members, with Recharts KPIs.
- Deployed with Docker, trained the staff at both locations and provide ongoing support.
Stack by layer
Interface
- Next.js
- React
- TypeScript
- Tailwind CSS
- Recharts
Backend
- NestJS
- Node.js
- Prisma
- Zod
- JWT
Data and queues
- PostgreSQL
- Redis
- BullMQ
- Socket.IO
Infra and quality
- Docker
- Docker Compose
- Jest
- Playwright
- Swagger
What I take from this project
In a custom project, discovery is half the product. The rules that mattered most, like plans valid at both locations or weekly access limits, only surfaced by talking to the front desk at peak hours.
Rolling out also means training. Adoption at both locations came when each role got a screen designed for their day, not one dashboard with everything for everyone.
Money calls for idempotency from day one. Treating repeated webhooks and duplicate charges as the rule, not the exception, kept every entry in the books single.