Client type
Premium golf simulator club (physical + digital)
Markets
Midland · Lubbock · Big Spring, Texas
Engagement
Full-stack build — sole author, web + mobile + backend + database
Status
Shipped
Next.js 15
React 19
TypeScript
MUI v7
Prisma 6
MySQL
NextAuth.js
React Native 0.81
Stripe
bcryptjs
The problem
The client operated a premium golf simulator club with physical locations in three Texas cities. Each location had a fixed number of simulator bays — physical rooms with hardware — and members needed to book time on them. The existing process was manual: phone calls, spreadsheets, and staff at each location keeping their own booking log. It didn't scale, and it was starting to double-book bays.
The specific complication was the membership model. Rather than a single flat rate, the club sold three tiers — Gold, Platinum, and Vibranium — each with genuinely different booking rules: how far in advance you could book, how many concurrent bookings you could hold, how long each session could be, and whether weekend slots were included. A naive rules table wouldn't cover it — the rules interact (a Gold member on a Sunday triggers different constraints than a Gold member on a Wednesday) and they need to be enforced at booking time, on the waitlist, and on the admin override.
Ship a full booking platform — web for members and staff, mobile for members on the go — that handles three membership tiers across three physical locations without double-booking a single simulator bay.
The approach
Built as a single codebase for the web app and a separate React Native app for mobile, both talking to one Prisma/MySQL data model. Four moves made it work:
1. Membership rules as a first-class engine
The tier rules aren't sprinkled through the booking code — they live in a dedicated membership.ts module that answers questions like "can this user book this bay at this time for this duration?" The rest of the system just asks that module. Adding a new tier or changing a rule means editing one file; nothing downstream cares.
The rules table looks like this in practice:
| Tier | Advance booking | Concurrent bookings | Session length | Weekend access |
|---|---|---|---|---|
| Gold | Current week only | 1 | 30 minutes | No |
| Platinum | 2 weeks | 3 | 1 hour | Yes |
| Vibranium | 2 months | 10 | 3 hours | Yes |
2. Real-time bay availability with conflict detection
A physical simulator bay is a hard resource — two members cannot be in the same bay at the same time, regardless of what the system thinks. Every booking write goes through a conflict-check against the current state of that bay's schedule, at transaction time, in the database. The API refuses the write if there's a conflict; the client re-fetches availability and offers alternatives.
3. Waitlist as an automatic queue
Popular slots (Vibranium prime-time weekend at Midland, for example) fill up. Rather than tell members "sorry, try again," the system lets them join a waitlist. When a booking is cancelled or a session ends early, the waitlist processes automatically — matches the next eligible member (respecting their tier rules) and books the slot for them, with a notification.
4. One data model, two clients
The web app (Next.js) and the mobile app (React Native) both hit the same Prisma-backed API. The mobile app handles Stripe payments natively; the web app handles admin flows (creating members, running the waitlist manually, resolving disputes). Everything else — bookings, dashboard, membership details — works on both, from one truth.
What shipped
- Web platform: Next.js 15 App Router, MUI v7, member dashboard, booking flow, waitlist UI
- Mobile app: React Native 0.81, native iOS and Android, Stripe payment integration
- Prisma/MySQL data layer with User, Location, Bay, Booking, and Waitlist models
- NextAuth.js authentication with role-based access — member, location admin, super admin
- Membership rules engine enforcing tier constraints across every entry point
- Real-time conflict detection at the database transaction layer
- Automated waitlist processing on cancellation and session end
- Location-scoped admin — Midland admins can only manage the Midland location
- Test-user seeding scripts (Gold, Platinum, Vibranium, per-location admins) for QA and demos
What made the release own-able
Physical-inventory booking systems are unforgiving. A double-booked simulator bay isn't a soft failure that a user retry fixes — it's a real customer standing in a real hallway wondering why the staff have no room for them. The whole system has to be correct, not just usually correct.
Being sole author on the full stack meant no handoff seams where correctness could slip between teams. The membership rules that block a booking at the API also inform the availability UI on the web, the availability UI on the mobile app, the waitlist matching logic, and the admin override warnings. All in one head, all consistent.
When the resource being booked is physical, correctness isn't a nice-to-have — it's the whole product. One person holding the full state model beats a five-person team where nobody owns the invariants end-to-end.
Stack details
Web platform
- Framework: Next.js 15.4 with App Router and Turbopack dev server
- UI: React 19, MUI v7 with custom theming, MUI DataGrid for admin tables, MUI Date Pickers for booking flows, Recharts for admin analytics
- Auth: NextAuth.js 4.24 with the Prisma adapter, credential provider, JWT sessions, bcryptjs password hashing
- Data: Prisma 6 ORM, MySQL 8 database, transaction-wrapped booking writes
Mobile app
- Framework: React Native 0.81, React 19, TypeScript
- Navigation: React Navigation v7 (bottom tabs + stack)
- Payments: Stripe React Native SDK
- Forms: React Hook Form for booking + profile flows
- Maps + UI: React Native Maps for location display, Reanimated 4 for interactions, Linear Gradients, Vector Icons
Ops
- Prisma migrations for schema evolution
- Dedicated seed scripts for basic seed, complete-user seed, and demo seed
- Admin creation script for provisioning per-location admins
Have a similar problem?
If you're building a booking or scheduling platform where the resource being booked is physical — hardware, rooms, vehicles, appointments — we've done this. The correctness bar is different from digital-only bookings, and it's what most systems get wrong.
Talk to us →