Work
Studio Management BUILT AND DEMO-READY, NOT YET LIVE IN A STUDIO

One system for a contrast-therapy studio: a booking app members love and an ERP that actually runs the place

A dual-sided operating system for contrast-therapy studios. Members get a clean mobile booking app. Owners get a deep back office that handles bookings, CRM, loyalty, inventory, maintenance, staffing, and reporting. One codebase, one database, one login model.

~50Prisma models behind 20 modules and 150+ API endpoints
Book a call

The problem

Boutique wellness studios (sauna, cold plunge, contrast therapy) usually run on a stack of half-fitting tools: one app for booking, a spreadsheet for the member list, a group text for staff, a separate thing for inventory, and nothing real for loyalty or maintenance. None of it talks to each other, so the owner becomes the integration layer. A member books a slot in one place, but the front desk, the credit balance, the loyalty points, and the revenue report all live somewhere else. We set out to build the whole thing as one operating system: a member-facing app and a full admin ERP sharing a single database, so a booking, a credit deduction, a loyalty entry, and a revenue number are all the same event.

What we built

The modules of the system.

01

Member booking app

Mobile-first member experience. A dashboard with credit balance and next booking, a 5-step booking wizard (location, service, date/time, review, pay), a wallet for buying credits and packs, a schedule with cancel and reschedule, a learning hub, and 1:1 messaging with studio staff.

02

Calendar and bookings

Day, week, and month views with slot-density toggles and block management. The server generates bookable slots from each service's own rules (duration, interval, buffer, hours, days) and checks live capacity before confirming, so a slot can never be overbooked past its room limit.

03

CRM hub

Member directory with deep profiles (lifetime value, timeline, notes), a drag-and-drop lead pipeline with stages from new lead to converted, a campaign builder across email, SMS, push, and in-app ribbon, segments, and automations.

04

Loyalty and referrals

A configurable points engine: earning rules by spend, visit, milestone, and referral, plus a rewards catalog (credit, discount, item, service), a points ledger, and a referral engine. Redemptions write back to the same credit-transaction ledger that bookings use.

05

Operations center

Tasks (kanban and list), inventory with low-stock alerts, purchase orders, and vendors, maintenance with work orders and preventive-maintenance schedules, and staffing with rosters, shifts, PTO, and compliance tracking.

06

Reports and settings

A 5-tab analytics suite (operations, revenue, members, marketing, finance) computed server-side, plus a custom report builder. Settings cover roles and access levels, pricing plans, discounts, taxes, policies, consents, integrations, and notification events, each with its own admin-gated endpoints.

How it fits together

There are two repos and one system. The frontend (React 18, TypeScript, Vite, Tailwind) is a single app that renders either the member experience or the admin ERP depending on who logs in. The backend (Node, Express, TypeScript, Prisma over PostgreSQL) exposes everything under /api/v1 across 20 feature modules. Every module follows the same shape: a router, a controller and service, and a Zod schema that validates input at the edge. The database is the spine: roughly 50 Prisma models and 40-plus enums map almost one-to-one to the frontend's types, so a Booking, a Contact, a PricingPlan, or a LoyaltyReward means the same thing on both sides of the wire. Auth is JWT with a short 15-minute access token and a 30-day refresh token; admin-only routes sit behind an authorize('ADMIN') middleware. Stripe handles subscriptions, packs, and one-time payments, with its webhook mounted before the JSON body parser so signatures verify against the raw body. The frontend API client carries the token, refreshes it once on a 401, and replays the request, then normalizes backend enums (ADMIN to admin, GOLD to Gold) so the UI never has to think about wire format.

React 18TypeScriptViteTailwind CSSNode.jsExpressPrisma ORMPostgreSQLZodJWTStripeReplit

Under the hood

The decisions that mattered.

Slots are generated, not stored, then checked for capacity at booking time

Rather than pre-creating a row for every possible appointment, each service carries its own scheduling rules: slot duration, interval, buffer, start and end times, and which days it runs. A generateSlots utility turns those rules plus a date into the list of bookable windows on the fly, skipping days the service does not run. When a member opens a date, the server generates the slots, counts the confirmed and pending bookings already sitting on each start time, and returns availability as maxCapacity minus booked. On the write path the server re-counts before it commits and rejects the booking if the slot is already full, so capacity is enforced at the moment of truth rather than from a stale availability view. This keeps the schedule flexible (change a service's hours and every future slot updates) without a table full of empty slot rows to maintain.

Booking, credits, and membership tier are one transaction

A booking is not just a calendar entry, it is an accounting event. When a member books, the server reads their membership tier and decides what to charge: drop-in and unmanaged members spend a credit, Silver members spend a session from their cycle, and Gold members book unlimited with no deduction. Whenever a credit is spent it both decrements the member's balance and writes a row to the CreditTransaction ledger with a USAGE type and a human-readable description. That same ledger records purchases, expiries, manual adjustments, and loyalty earn and redeem, so the wallet a member sees and the revenue a report counts are reading one source of truth instead of two systems that drift apart.

Twenty modules, one predictable shape, so the surface stays legible at scale

Running a studio touches a lot of unrelated concerns: bookings, payments, CRM, loyalty, leads, inventory, maintenance, staffing, documents, content, and reporting. To keep 150-plus endpoints from turning into spaghetti, every module follows the same router-to-controller-to-service-to-schema pattern, with Zod schemas validating input before it ever reaches business logic. Simpler modules collapse into a single router file; richer ones (auth, bookings, pricing, locations, services, users) split out a dedicated service layer. Authorization is consistent and boring on purpose: read routes are open to any authenticated user, every write route that changes studio configuration is wrapped in authorize('ADMIN'). A new feature is a new folder that looks exactly like the last one.

One frontend, two products, sharing a typed contract with the backend

The member app and the admin ERP are not two codebases, they are two faces of the same React app keyed off role. That only works because the frontend types and the Prisma models were designed together, close to one-to-one, and because a single API client owns the wire concerns for all 20 modules: it attaches the bearer token, transparently refreshes once on a 401 and replays the failed request, and normalizes the backend's uppercase enums into the friendly casing the UI expects. The design decision early on was strict: no mock-data fallbacks. When the team wired the frontend to the API, mock imports were stripped out of roughly 40 files so that what you see in the app is always what is actually in the database, never a hardcoded stand-in hiding a missing endpoint.

Release log

What we shipped.

Frontend ERP

Built the full dual-sided React app: member booking experience plus the admin modules (calendar, CRM, operations, reports, settings) running on mock data.

Backend build

Stood up the entire server in a focused push: 20 Express modules, the ~50-model Prisma schema with 40-plus enums, JWT auth with refresh-token rotation, Stripe integration, and 150-plus endpoints under /api/v1, seeded with test data.

API integration

Wired the frontend to the live API: a central client with token refresh and enum normalization, a data-fetching hook, and a deliberate purge of mock-data imports from roughly 40 files so the UI reflects only real backend state.

The outcome

Ritual Studio OS is a complete, demo-ready operating system for contrast-therapy studios: a member app and an owner-facing ERP built on one database, one auth model, and one set of types. The architecture is the point. Because a booking, a credit, a loyalty entry, and a revenue figure are the same data, the owner stops being the glue between five disconnected tools. The backend is built and seeded, the frontend is wired to it end to end with no mock fallbacks, and the system is ready to onboard its first studio. It is not yet live in production with a paying client; the next steps are real-world end-to-end testing and rollout.

Put your business on autopilot