Documentation/Reference apps/Gym class booking app PulseFit keeps its waitlist in a single field
Gym class booking app PulseFit keeps its waitlist in a single field
PulseFit is an open-source gym class booking app with a weekly timetable, a waitlist and a member portal. Source on GitHub, live at pulsefit.supero.live.
- apps
- fitness
- bookings
- event-workflows
- single-tenant
Overview
Five schemas make this open-source gym class booking app for a four-club brand, with a weekly timetable kept as text and a booking that is confirmed by the act of creating it.
Reformer Sculpt runs on Tuesday at 9:00 AM at PulseFit Marina. It has 12 places and none left. On 2 October 2026 the public class endpoint on pulsefit.supero.live returned that class with
spots_left at 0, and ui/app.js labels any class at zero "Waitlist" and changes its button to "Join waitlist". All of the code is in the repo's pulsefit folder on GitHub.Press it and the app creates a booking like any other. The only difference is one field.
python
ClassBooking = {
"schema_type": "object", "name": "ClassBooking", "namespace": "pulsefit", "parent_type": "tenant",
"description": "A member's booking for a class with its lifecycle state.",
"attributes": [
{"name": "class_name", "type": "string"},
{"name": "member_name", "type": "string"},
{"name": "member_email", "type": "string"},
{"name": "club_name", "type": "string"},
{"name": "day_time", "type": "string"},
{"name": "booking_state", "type": "string", "mandatory": True,
"values": ["booked", "attended", "cancelled", "waitlist"]},
{"name": "owner_username", "type": "string"},
],
}There is no waitlist table and no queue. A waitlisted member holds a
ClassBooking whose booking_state is waitlist. It shows in their portal under My bookings with a Cancel button beside it, exactly as a confirmed one does. I searched the 19 schemas.py files in the repo; PulseFit's is the only one with a waitlist state.A timetable with no datetime in it
schemas.py defines five schemas, one per table:
Club, Trainer, ClassOffering, Member and ClassBooking. None of them has a datetime attribute. A class's slot is day_time, a string, and the seed data in setup.py fills it with values like "Mon · 6:30 AM".That models a gym whose week repeats. Sunrise HIIT is a Monday class, not a class on a particular Monday. The filter on the classes page finds the weekday by taking the first three letters of the string. It is a small trick and it removes a whole category of work: no session generation, no time zones, no past sessions to archive.
The seed has 4 clubs, 8 trainers and 14 class offerings, and the three public endpoints on the live app returned those same counts when I fetched them that day. Those three schemas are the public ones, so the club pages and the full schedule are readable before anyone signs in.
Records point at each other by name. A class carries
club_name and trainer_name as strings, and no schema declares a reference.A gym class booking that confirms itself
Nobody on staff approves a PulseFit booking. The member creates it in state
booked, and two event bindings in setup.py do the rest:python
{"event": "@create:pulsefit:class_booking", "workflow_id": "booking_confirmation",
"input_map": {"member_email": "user.email", "member_name": "member_name",
"class_name": "class_name", "club_name": "club_name", "day_time": "day_time"}},
{"event": "@create:pulsefit:member", "workflow_id": "welcome_member",
"input_map": {"member_email": "user.email", "member_name": "full_name",
"plan": "plan", "home_club": "home_club"}},Creating a
class_booking starts booking_confirmation, which emails the member. Creating a member starts welcome_member. In both bindings the recipient is user.email, the address of the signed-in account that performed the create.Access is set by two policies. A
tenant_user, the member, reads the three public schemas. On class_booking the member has every permission short of delete, limited to rows where owner_username matches their login. That is policy-based row scoping, done by the server. On member the rule grants read and create and stops there. With no update permission, members cannot edit their own plan or member_state after joining. Changing either is a job for staff, whose tenant_admin policy does include update.The four membership plans sit in one constant in app.js, from a $29 Day Pass to a $219 Elite tier.
stripe_checkout is in the services list in config.py, and in the code on main, choosing a plan passes its price to services.stripe.checkout and redirects to the checkout URL that comes back.Four clubs in one tenant, where Pulse uses four tenants
PulseFit has a sibling in the repo with nearly the same name. Pulse is also a gym chain, and the two disagree about what a location is.
In PulseFit a club is a row. config.py declares a single tenant, and the staff console has an "All clubs" dropdown that narrows the schedule, trainer and member lists by comparing
club_name or home_club in the browser. The dashboard's bookings-by-club chart is a count over one list. One staff login sees the whole brand.Pulse makes each location a tenant, with an HQ tenant above them, nine schemas, dated sessions with start and end times, four schemas built on platform service bases, and a staff confirmation step before the member is notified. That is the right model when locations must be kept apart from each other and the wrong one when a single owner wants to change Thursday's timetable across the city in one sitting, which is the case PulseFit is built for.
From clone to a full Tuesday class
The source folder is apps/fitness/pulsefit.
bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/fitness/pulsefit
cp .env.example .env # set SUPERO_DOMAIN and SUPERO_PASSWORD in it, then:
./run.shNo account is needed, since domain registration is open. PulseFit is running in about two minutes at
http://localhost:5670. The files you cloned are MIT licensed, and the Supero platform they call is a hosted service that is not open source.The sign-in form at pulsefit.supero.live lists two demo accounts: one member, one staff user. As the member, join the waitlist for Reformer Sculpt and find the booking under My bookings. Then edit
values on booking_state in schemas.py, run ./run.sh --reset, and you have changed the lifecycle of a gym.Build the next one from your own schema at supero.dev.
On this page