MGR

MGR

MGR is the operating system for a fitness studio. Not a booking widget - the whole back office: the class schedule, the memberships and packages that pay for it, the point of sale, the staff and payroll inputs, the marketing, the reporting, and every message the studio sends its members. A studio signs up as a tenant, and under it sit multiple locations, role-scoped staff, a product catalogue and a member base whose bookings, payments and attendance all have to reconcile. Six client surfaces run on one API, and the system has been shipping continuously since 2018.

Project Summary:

Client
MGR
Category
Multi-Tenant SaaS
Industry
Fitness Studio Management
Inclusions
1 API, 2 web apps, 3 mobile apps
MGR
6
client surfaces served by one API
2018
first commit, and still shipping weekly
745
command objects, each declaring its own permitted actors
165
database tables behind the scheduling and money model
192
background workers, 46 of them on a schedule
1,013
spec files gated in CI on every pull request

What We Built

One Rails API behind an admin console, a member portal, member and staff mobile apps, a front-desk check-in kiosk, embeddable booking widgets, and a partner API that ClassPass calls directly.

SurfaceBuilt withWho uses it
Admin console Next.js 15, React 19, TypeScript, MobX Studio owners, managers and front desk
Member portal React 18 SPA, Zustand, embeddable in the studio's own site Members, and anonymous visitors browsing a schedule
Member mobile app React Native on Expo, iOS and Android Members booking, paying and checking in
Staff mobile app React Native on Expo Owners and instructors away from the desk
Check-in kiosk React Native on Expo, iPad-gated in code Members arriving for a class, unattended
Partner and embed Partner API, booking widgets, hosted landing pages ClassPass, and the studio's own marketing site

Why This Was Hard

The calendar is the easy part. What makes studio software hard is that the money has to stay correct while the schedule moves underneath it. "Cancel the charge" means two different operations depending on whether money has already left the member's account: an upcoming transaction is voided, an executed one is refunded, and a refund can be line-item-level on an order or standalone on a transaction. Freezing a membership does not just pause billing, it shifts every future package instance forward by the length of the freeze. Booking someone into a paid class without a package does not create a comp, it creates a debt that has to be settled at the next sale. Get any of these subtly wrong and the studio finds out from its accountant, months later.

The second difficulty is surface count. A studio owner on a laptop, a member on a phone, an instructor on a tablet, an unattended kiosk in the lobby and a partner booking platform all need to perform overlapping subsets of the same operations. The usual outcome is authorization logic smeared across controllers, with each client re-deriving what it thinks its user is allowed to do, and a permissions bug that everyone knows about and nobody wants to touch.

The third is age. The API's first commit is from August 2018 and the newest frontend's is from February 2026, and both were committed to in the same week. Every decision here had to be one that could be made incrementally, on a system that was already carrying a live customer base, with no cutover weekend available.

How It Is Put Together

01

Multi-tenancy that stayed simple on purpose

Row-level isolation on a single tenant column, applied through one concern included in 114 of the 248 models. No schema-per-tenant, no database-per-tenant, no Apartment or acts_as_tenant. That is a deliberate choice rather than a shortcut, and the part that makes it defensible is where tenant resolution happens.

A Rack middleware runs before any controller and resolves the tenant from the request origin, falling back to a custom-domain lookup for studios on their own hostname, and to an explicit parameter for mobile and partner clients that have no browser origin. Controllers never resolve a tenant; they read one that has already been established. The same concern validates that associated records belong to the same tenant, so a cross-tenant reference fails at the model layer instead of quietly leaking. That check is the answer to "how do you know a query never crosses tenants".

02

One authorization model across every surface

Every business operation in the system is a command object - 745 of them across around 98 domain folders - and each one declares the actor types and permissions allowed to run it. The check-in iPad, the member's phone, the admin console and the partner API all enter through the same door.

The practical consequence is that adding a seventh surface does not mean re-auditing permissions across a controller layer. It means issuing a token with the right actor type. Commands compose, emit activity-log entries as a side effect, and return validation failures as structured data rather than exceptions, so every client renders the same errors the same way.

03

The money model, and the vocabulary underneath it

Void versus refund, suspensions that shift future billing, unpaid visits carried as debt, proration, account balances, gift card instances, promotions that re-apply on renewal, per-location sales tax, late-cancel and no-show penalties, payment method migration, and a revenue ledger that has to reconcile against all of it. 165 tables and 466 migrations is what that costs.

A large part of the work here was not code but vocabulary: an internal domain document runs to a couple of hundred lines of "these two words mean the same thing" and "these two things sound identical and are not". That document is now also what the in-app assistant retrieves from, which is the cheapest possible return on having written it down.

04

Asynchronous work, and real time without ActionCable

192 background workers, 95 of them dedicated to notifications, and 46 scheduled jobs that run the parts of the business that must happen whether or not anyone is logged in: package renewals, booking reminders, suspension expiry, email suppression checks, revenue computation.

Real-time updates go over server-sent events on Redis pub/sub rather than ActionCable - one connection per session, namespaced per tenant and per receiver, fanned out to an in-app pub/sub on the client. Reservations, orders, conversations, typing indicators, notifications and reviews all ride the same channel.

Where customers author their own automation, the system uses event sourcing: domain events publish to global, per-user and per-tenant streams, and a subscriber drives the workflow engine. It is event-sourced only there, where a stable public event vocabulary genuinely earns its complexity.

05

Search, and retrieval for the assistant

Elasticsearch indexes 19 models to power a single global search across clients, staff, schedules, packages, retail products, gift cards and promotions - the search in the screenshots above.

Alongside it, pgvector holds 1,536-dimension embeddings of the product's own support documentation, chunked and indexed so the assistant answers from what the product actually does rather than from what a model assumes fitness software does.

06

Two AI products, shipped into a live system

The first generates notification templates: a studio brands all 114 of its transactional messages in under half an hour instead of hand-editing 82 emails, with validated LLM output, a bounded repair loop, and a drafts table so nothing goes live without a human approving it. There is a full write-up of that work, including the failure modes.

The second is a multi-agent admin assistant: three agents with handoff between them - support, read-only question answering, and a mutating admin agent - over 40 tools, retrieval across the support documentation, and tracing on every handoff and tool call. Its write operations go through the same commands, and therefore the same permission checks, a human would hit.

The point worth making is not that the system uses AI. It is that both of these landed as ordinary pull requests on an eight-year-old codebase, with no rewrite and no cutover.

07

Two frontends, two state libraries, on purpose

The admin console and the member portal share an API and a tenancy model but disagree on framework generation and state library, and that is a decision rather than drift. The console optimises for a staff member who keeps a tab open all day: client-rendered, store-owned data, 117 observable stores and permissions applied down to individual form fields.

The member portal optimises for the opposite - first paint on a page that sits next to a studio's marketing site. It supports guest-first browsing, where an anonymous visitor can look at a schedule and prices before an account exists, and an embed mode where the whole app runs in an iframe on the studio's own domain, with an origin whitelist, a height handshake and token passthrough so an already-signed-in member is not asked to log in twice.

What We Delivered

  • Six client surfaces on one API - admin console, member portal, member and staff mobile apps, a check-in kiosk and embeddable widgets - all entering through the same authorization model.
  • A complete back office: scheduling and rosters, memberships and packages, point of sale and retail, gift cards and promotions, waivers, questionnaires, reviews and marketing.
  • Reporting in two generations kept side by side through the migration, plus analytics on trial conversion, retention and churn.
  • A workflow automation designer, a widget and landing page builder, and a CSV import wizard, so studios can configure the product rather than file tickets against it.
  • Two AI products in production: notification template generation and a multi-agent admin assistant with retrieval over the product's documentation.
  • End-to-end tests running in CI against a full stack - Postgres, Redis, Elasticsearch, the API and the web app all booted and signed into through the real UI.

Tech & Integrations

API
  • Ruby 3.3
  • Rails 7.1 (API-only)
  • Puma
  • Sidekiq 7.3
  • RailsEventStore
Data
  • PostgreSQL 16
  • pgvector
  • Redis 7.2
  • Elasticsearch
  • S3
Admin console
  • Next.js 15
  • React 19
  • TypeScript
  • Tailwind 4
  • Radix
  • MobX 6
Member portal
  • React 18
  • React Router 6
  • Zustand 5
  • styled-components
Mobile
  • React Native
  • Expo
  • EAS Build
  • Stripe React Native SDK
AI
  • OpenAI
  • Multi-agent handoff
  • pgvector retrieval
  • Langfuse tracing
Payments and messaging
  • Stripe
  • SendGrid
  • Twilio
  • OneSignal
Integrations
  • ClassPass
  • Mindbody import
  • Mailchimp
  • Constant Contact
  • Google Maps
  • Unlayer
  • Froala
Quality and operations
  • RSpec
  • Playwright
  • Sentry
  • New Relic
  • PGHero
  • Flipper
More Work

Explore Other Projects

OpiGo project by Boolean Solutions
Consumer Fintech

OpiGo

A social network for stock predictions where every call is scored against the market. We built the Android app and ran the infrastructure through the growth from launch to 10,000+ users; OpiGo was acquired by Times Network in 2026.

Industry
Fintech: Retail investing community
Delivered
Android app, web app support, infrastructure
View project
PADS4 project by Boolean Solutions
Microsoft Tech-Stack

PADS4

Smart-building workspace and visitor management for enterprises including Siemens, Samsung and Bangalore Airport, delivered across Outlook, Gmail, Teams and the web.

Industry
Workspace Booking Management
Delivered
1 Outlook + Gmail Plugin, 1 Teams App, 2 web apps
View project
ReviewMaiden project by Boolean Solutions
Analytics & AI

ReviewMaiden

Reviews aggregated from 70+ platforms in real time, run through sentiment analysis and categorisation so a brand can see the themes behind its rating.

Industry
Martech: Online reputation management
Delivered
Web app with AI and Analytics
View project