Openfront Gym
Organization-scoped memberships, classes, rosters, check-in, personal training, retail, billing, and member operations.
Source · Live site · Catalog
Openfront Gym is a self-hostable app for gyms and fitness studios. It has a public site for memberships and class booking, member accounts, a staff dashboard, and a staff-unlocked check-in kiosk. It runs on Next.js 16, React 19, Keystone 6, Prisma, and PostgreSQL.
To install it, see Getting started.
Surfaces
- Storefront (
/,/classes,/schedule,/memberships,/join,/instructors,/facilities,/policies,/waiver,/contact). See External storefronts. - Member account (
/accountand sub-pages): bookings, membership, profile, participation terms, personal training, and an instructor view for coaches. - Member check-in code (
/member/check-in-code): a QR code that rotates every 30 seconds. - Kiosk (
/kiosk/check-in): member search, QR or manual member check-in, and guest check-in. - Staff dashboard (
/dashboard/platform/*plus generic list pages at/dashboard/<list-key>). See External dashboards.
Tenancy and roles
Organization is the tenant root. A signed-in session carries one organization. Operational records have a required organization relationship, and access rules filter by it. There is no organization switcher.
The storefront serves one organization. It uses STOREFRONT_ORGANIZATION_ID when set. Otherwise it uses the only active organization. With more than one active organization and no setting, the public catalog is empty.
An organization can be active or suspended. A suspended organization keeps sign-in, password recovery, reads, and reconciliation of existing payments. New bookings, sales, check-ins, catalog edits, settings edits, and new charges are refused. Organization currency is fixed to USD.
Roles carry capability flags: canCreateRecords, canManageAllRecords, canSeeOtherPeople, canEditOtherPeople, canManagePeople, canManageRoles, canAccessDashboard, canManageOnboarding, canManageSettings, canManageAppointments, canManageCheckIns, canManageFacilities, canManagePrograms, canManageCommunications, canManageRetail, canManagePayroll, canViewReports, and isInstructor. A user cannot grant a role capability they do not hold unless they have canManageAllRecords.
Data model
Models registered in features/keystone/models/index.ts:
| Area | Models |
|---|---|
| Tenant and people | Organization, User, Role, Member, MemberImportRecord, ContactMessage |
| Settings and sites | GymSettings, Location, GymResource |
| Memberships | MembershipTier, Membership, MembershipCreditGrant, MembershipCreditEntry, Subscription |
| Classes | ClassType, ClassSchedule, ClassInstance, ClassBooking, Waitlist, AttendanceRecord, Instructor |
| Check-in | CheckIn |
| Personal training | TrainerAvailability, TrainerAppointment, TrainingPackage, TrainingCreditEntry, CoachingAssignment, TrainingLead |
| Workouts | Exercise, WorkoutLog, WorkoutSet |
| Payments | MembershipPayment, GymPayment, PaymentMethod, PaymentProvider, PaymentSession, PaymentEvent, MembershipBillingAttempt, GymRefundAttempt, CounterPaymentAttempt |
| Retail | RetailItem, RetailSale, RetailReturn, RetailStockEntry, RetailClose |
| Operations | OperationalNotice, ParticipationPolicy, ParticipationEvidence, OperationsCase, IntegrationCredential |
| System | OnboardingRun, StaffIntent, AuthRateLimitBucket |
Some notes on how they fit:
- Class booking works on dated
ClassInstancerows. Each booking isconfirmed,waitlist, orcancelled. The separateWaitlistmodel is an older schedule-level list; live booking uses the per-session waitlist onClassBooking. - Class credits come from a
MembershipCreditGrantper service period, with every change inMembershipCreditEntry. - Training packages hold prepaid credits. Each use, refund, or adjustment is a
TrainingCreditEntry. StaffIntentstores the exact form request a person submitted, so a lost response can be retried without applying it twice.CounterPaymentAttemptrecords a counter card sale or refund before Stripe is called.
Named GraphQL operations
Custom operations are defined in features/keystone/mutations/index.ts and the files it merges. Generated Keystone CRUD is also available, subject to model access rules.
Public storefront queries (scoped to the storefront organization, no member or payment data): publicGymSettings, storefrontOrganization, publicGymClassTypes, publicGymClassType, publicGymSchedules, publicGymSchedule, publicGymClassInstances, publicGymClassInstance, publicGymInstructors, publicGymInstructor, publicGymMembershipTiers, publicGymMembershipTier.
Member account: memberProfile, updateMemberProfile, memberCheckInCode, myUpcomingBookingPage, registerMember, changeMemberPassword, requestEmailChange, confirmEmailChange, refreshMyEntitlement.
Classes and scheduling: checkClassAvailability, bookClass, cancelClassBooking, promoteFromWaitlist, cancelClassInstance, markClassAttendance, schedulingWorkspace, saveGymClassSchedule, saveGymClassInstance, generateUpcomingClassInstances (1 to 12 weeks), updateClassScheduleCapacity, updateClassInstanceCapacity, rosterSessions, rosterDetail, instructorAccount.
Check-in: frontDeskWorkspace, recordMemberCheckIn, checkOutMember, authorizeKioskSession, kioskSearchMembers, kioskRecordMemberCheckIn, kioskRecordGuestCheckIn.
Personal training: trainingWorkspace, bookTrainerAppointment, rescheduleTrainerAppointment, transitionTrainerAppointment, saveTrainerAvailability, recordTrainingPackage, refundTrainingPackage, assignCoaching, transitionCoaching, saveTrainingLead, eligibleCoachAccounts, createCoachProfile.
Billing: getBillingStats, billingWorkspace, initiateMembershipCheckout, completeMembershipCheckout, createStripeSetupIntent, getStripeBillingPortal, cancelMembership, freezeMembership, unfreezeMembership, changeMembershipTier, markPaymentRecoveryContacted, refundGymPayment, replayPaymentEvent.
Retail and counter card payments: retailWorkspace, retailSaleItems, saveRetailItem, quoteRetailSale, sellRetail, returnRetail, closeRetail, reconcileCounterPayment.
People and settings: inviteMember, setMemberAccountStatus, importMemberContacts (1 to 100 rows, with dry run), gymSettingsEditor, upsertGymSettings, publishGymParticipationTerms, reportsDashboard, submitContactForm.
Operations: operationsWorkspace, participationWorkspace, runOperationsCommand, exportOperatingData (payments, members, or cases), updateGymResourceAllocation, reconcileGymEntitlements.
Partner discovery: discoveryClasses, discoveryBookClass.
Onboarding and system: runDeterministicOnboarding, transitionOnboardingStatus, redirectToInit, beginStaffIntent, settleStaffIntent, myStaffIntent, myPendingStaffIntents.
Key workflows
Joining. A visitor picks a plan and cycle on /join. If participation terms are published, they accept them first. The server action calls initiateMembershipCheckout, which opens a Stripe-hosted subscription checkout. On return, /join/success calls completeMembershipCheckout to provision the membership. Without Stripe, members join at the front desk.
Booking a class. bookClass checks, inside one transaction: same organization, session not cancelled or past, schedule and location active, member active and linked to a user, no duplicate booking, an active membership, remaining class credits, and the room's state. A full session puts the member on the waitlist. Cancelling a confirmed booking can promote the next waitlisted member.
Check-in. Staff record entry and exit on /dashboard/platform/check-in. The kiosk exchanges KIOSK_API_TOKEN for a 12-hour HttpOnly session cookie, then checks members in by search or QR code, and records guests with an idempotency key.
Personal training. Staff set trainer availability and sell packages (cash or settled tender, or card through a Stripe counter checkout). Members with a package can book, reschedule, and cancel appointments from /account/training. Coaching assignments move between staff and member.
Retail. Staff keep items per location, take sales and returns, record stock entries, and close the drawer with a variance reason.
Operations. The Operations page handles member notices, cases (incident, inspection, facility closure, financial reconciliation, integration exception, privacy review), policy and waiver records, facility resources, partner credentials, data exports, and payment event replay.
Onboarding
The onboarding dialog calls runDeterministicOnboarding with template minimal, full, or custom. The whole run is one database transaction under a per-organization lock, so a failed run leaves nothing behind. It creates or reuses:
- store settings (an existing record is kept; only a missing logo icon is filled);
- one location;
- membership tiers and class types;
- a bounded instructor role (dashboard access and
isInstructoronly); - instructor users and profiles;
- weekly class schedules at that location;
- dated class sessions for the next 14 days;
- a Stripe
PaymentProviderrecord.
Instructor users get a reserved example.invalid address and a random password, so nobody can sign in as them. Sessions that conflict with a coach, room, closure, or qualification are skipped and listed in the result. Setup never publishes participation terms; publish your own from Store Settings.
A completed run is not re-applied. Running it again with the same configuration returns the stored result. A different configuration is refused; edit plans, classes, and coaches on their own pages instead.
The result includes a readiness checklist. Required steps are store settings, an open location, plans, class types, active coaches, and a schedule with upcoming sessions. When online checkout is on, at least one plan needs a Stripe product ID and a price ID above zero. These IDs are format-checked only, not verified with Stripe.
Integrations
Stripe is the only registered payment adapter (features/integrations/payment). It covers hosted subscription checkout, setup intents, the billing portal, cancel, freeze, resume, tier changes, refunds, and counter card checkouts for retail and training packages. Signed webhooks arrive at /api/stripe/webhook. Handled events include checkout session, subscription, invoice, charge refund and dispute, refund, and payout events. Online checkout requires exactly one installed Stripe merchant organization. Per-tenant connected accounts are not supported. PAYMENT_TEST_MODE swaps in a test adapter for isolated tests and is refused in production.
Mail is SMTP through MAIL_ENABLED and SMTP_*. It sends password reset links, email-change confirmations, email-changed notices, and staff copies of contact messages.
Partner discovery exposes GET /api/discovery/classes, POST /api/discovery/bookings, and GET /api/discovery/calendar (an iCalendar feed, up to 1,000 sessions). Partners send a key in x-discovery-api-key or a bearer token, plus x-discovery-partner. Keys come from the Operations page (gym_-prefixed, stored as a hash, with scopes classes:read and bookings:create) or from the DISCOVERY_* environment variables. Requests are rate limited.
MCP is served at /api/mcp-transport/[transport]. It forwards the caller's session cookie to GraphQL and offers generic tools: listModels, queryData, searchModels, getFieldsForType, lookupInputType, createData, updateData, deleteData, and modelSpecificSearch. Access is whatever the session's role allows.
AI chat in the dashboard calls /api/completion with OpenRouter when OPENROUTER_API_KEY and OPENROUTER_MODEL are set.
Storage is one S3-compatible bucket (my_images) for images such as member profile photos.