Documentation/Reference apps/One tenant per location in Pulse, a multi-location gym management app
One tenant per location in Pulse, a multi-location gym management app
Pulse is open-source multi-location gym software where each gym is its own tenant, with classes and bookings. On GitHub, live at pulse.supero.live.
- apps
- fitness
- multi-tenant
- bookings
- memberships
Overview
Open-source multi-location gym management software for a boutique fitness chain, where each location is a separate tenant, so Downtown's bookings and Westside's bookings never share a list.
CLASS_TEMPLATES in Pulse's setup.py has ten entries, from Vinyasa Flow Yoga to Strength Foundations. The seed loop, which loads the demo data, writes all ten into the downtown tenant, then again into westside, then again into harborpoint.Thirty rows for ten classes.
That is deliberate.
In Pulse a gym location is a tenant, the platform's unit for keeping one organisation's data apart from another's, and a tenant owns its own copy of everything, catalog included. The public landing page in ui/app.js reads the class catalog across all three and removes the repeats by
display_name before drawing the grid. The hosted demo's catalog also holds a few test rows, so its grid runs past ten. The source is the repo's pulse folder.What the duplication buys is on the other side of the login. A Westside manager's console lists Westside's sessions and Westside's members with no location filter anywhere in app.js. The header of schemas.py states the mechanism: every record is
parent_type: "tenant", so "the platform stamps the active tenant on create and filters reads by it." The repo README adds that the isolation is enforced by the server, for a user calling the API directly with their own valid token.A class type, a dated session, and a seat in it
Pulse has nine schemas, one per table, and three of them describe a class at different levels.
FitnessClass is the type: category, intensity, duration, capacity, drop-in price. ClassSession is that class at a time and in a room. Booking is one member's seat in one session.Four of the nine build on a platform service base.
ClassSession extends booking:base_booking, which supplies a mandatory status plus start_time and end_time, while Booking extends appointment:base_appointment and so gets a lifecycle of its own that runs from requested through confirmed to completed or cancelled. Membership extends membership:base_membership, and Payment extends payment:base_payment. In app.js one call to client.registerTransactionalExtensions, made after login, tells the SDK which schema sits on which base.I searched all 19
schemas.py files in the repo for the membership base. Pulse is the only app that uses it.Each location is seeded with the same eight-session schedule, with times computed from the moment you run setup:
python
session_plan = [
("vinyasa-flow", 0, 0, 18, "requested"),
("power-hiit", 1, 1, 19, "confirmed"),
("rhythm-ride", 2, 1, 7, "confirmed"),
("barbell-strength", 0, 2, 18, "requested"),
("reformer-pilates", 3, 2, 12, "confirmed"),
("boxing-conditioning", 4, 3, 19, "requested"),
("metcon-burn", 1, -1, 18, "completed"),
("sunrise-yoga", 2, -2, 7, "completed"),
]The second field picks one of the location's five trainers. The third is the day offset. Two of the eight are negative, which puts those sessions in the past and gives the schedule board some history on first load. A location therefore starts with one
Location row, 5 trainers, 10 class types and 8 sessions.Staff confirm, then the email and the text go out together
A member books from the Book a class tab. app.js creates a
booking with status requested, linked by reference to the session, and tells the member staff will confirm.Confirmation is a staff action.
In the console's Bookings tab the Confirm button updates the status and then runs the
booking_confirmed workflow. Its first step has "type": "parallel" and wraps an email call and an SMS call. Its second step writes workflow_status and processed_at back onto the booking, and the console shows that as a "notified" pill. Of the 19 apps, only two declare a parallel step in setup.py, and the other one is Medora, the hospital network.What a member can touch is set by the
tenant_user policy. Members read the catalog and the schedule. On the five private schemas, bookings and payments among them, they get only rows where owner_username matches their login. That is policy-based row scoping. One of those rules goes a level further:python
# STAFF-ONLY-FIELDS-V1 — a member reads their OWN booking, but the studio's
# internal notes about them are stripped server-side.
PolicyRule(entity="booking", can_read=True, can_create=True, can_update=True,
filter_field="owner_username", filter_match="$user.name",
hidden_fields=["notes"]),When a member reads their own booking,
notes is absent from the response body, removed server-side. Payments and check-ins are stricter in a different way: a member may create and read them, and the rule grants no update.In this multi-location gym app, HQ is the fourth tenant
config.py lists four tenants.
default-tenant is Pulse HQ and has no member-facing data. The three gyms follow.It seeds five accounts. A chain director sits on HQ. Downtown and Westside each have a manager and one demo member; Maya is Downtown's. Harborpoint has a full seeded schedule and no login of its own, so on a clone the director's account is the way to see it. The
TenantSwitcher component in app.js returns nothing unless client.canSwitchTenant() is true, and when it is, choosing a location calls client.setTenantOverride.PulseFit makes the opposite bet
The repo's other gym app, PulseFit, also has several locations. It keeps them all in one tenant. A club there is a row, each class carries a
club_name string, and the staff console narrows by club with a filter that compares strings in the browser. PulseFit has five schemas, extends no service base, and stores its timetable as text such as "Mon · 6:30 AM".Neither is the better gym app. A franchise, where each location is run by different people who should not be browsing another location's member list or payment history, gets that separation from Pulse's tenant-per-location model for the price of a repeated catalog. If one owner wants one screen for every club, PulseFit is less to carry.
Two minutes to three gyms
Everything above is in apps/fitness/pulse.
bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/fitness/pulse
cp .env.example .env # set SUPERO_DOMAIN and SUPERO_PASSWORD in it, then:
./run.shThe domain can be any free name, and no account is needed. The app comes up on port 5711 and is running in about two minutes. The code in the folder is MIT. Supero itself is a hosted platform and is not open source.
The login page of the hosted demo lists the demo accounts and also has a Join now link for self-service sign-up. On your clone, book a class as Maya, then sign in as the Downtown manager and confirm it.
To model a chain that is not Pulse's, write the schema file at supero.dev.
On this page