Skip to content
Back to work
2025 / case study

Multi-tenant SaaS for sports-school operations

Unified scheduling, bookings, and seasonal programs into one operating system for activity schools.

Domain
Product engineering · Multi-tenant SaaS
Role
Engineering owner, end to end
Stack
NestJSNext.jsPostgreSQLTypeORMAWS

The problem

Activity schools run on a surprising amount of glue. Instructor rosters live in one spreadsheet, customer bookings in another, seasonal enrolment somewhere else again, and the day's schedule gets rebuilt by hand every morning. It works until the season fills up, and then the seams start to show: double-booked instructors, students who fall out of the wrong list, and no single answer to "who is teaching what, when, and to whom."

I designed and built a tenant-isolated platform to be the operating system for that business — scheduling, bookings, and seasonal programs in one place — without leaking any one school's data into another's.

What I built

The platform is a few cooperating domains, kept deliberately separate.

Strict multi-tenancy. Every record is scoped to a tenant, and that scoping is enforced at the data-access layer rather than trusted to each query. A school can only ever see its own instructors, customers, and sessions. Isolation is a property of the system, not a convention people have to remember.

Two booking models under one roof. The product had to support both the legacy single-session booking flow (a product turns into a booking turns into a scheduled session) and a newer seasonal-program model (a program contains teams, teams meet on a recurring cadence, and students stay with their cohort across the season). Rather than bolt one onto the other, I modelled them as distinct domains with an explicit bridge, so neither flow had to pretend to be the other.

A real schedule view. The schedule is the screen the business actually lives in — day, week, and per-instructor swim-lane views built over the same underlying session data. Because everything reads from one source of truth, the schedule is always consistent with what was booked.

Attendance and skill tracking. For seasonal programs, instructors record attendance and per-student progress against a structured skill model, so a student's history travels with them through the season instead of resetting every session.

Role-based access. Admins, instructors, and (eventually) customers each see a different slice of the system, with authorization aligned to those roles rather than scattered through the UI.

Decisions and tradeoffs

Domain-driven design with a shared language. The booking world and the program world use different words for similar-looking things. Forcing them into one generic abstraction would have made every feature ambiguous. Keeping a precise, shared vocabulary per domain made the code read the way the business talks.

Enforce tenancy low, not high. It is tempting to filter by tenant in each query and move on. At this kind of scale, one forgotten filter is a data breach. Pushing isolation down into the data layer cost some upfront plumbing and removed a whole category of mistakes.

Bridge the models, do not merge them. Unifying the legacy and seasonal models into a single shape would have looked elegant and behaved badly. An explicit bridge between two honest models was easier to extend and far easier to reason about when edge cases appeared.

Outcome

The platform replaced a patchwork of spreadsheets and disconnected tools with a single, tenant-isolated system for running an activity school end to end — scheduling, bookings, and season-long programs with attendance and skill history. The domain separation and low-level tenancy enforcement gave it a foundation that new features could build on without re-litigating who-can-see-what every time.

Project details are kept deliberately general — delivered under NDA.

Next case studyAI-assisted resume evaluation for internal recruiting