Ellie and Craig were opening a children's play cafe in Guisborough. They had the keys, the vision, and the branding. What they didn't have was a way to run the business without drowning in admin.
Opening day was coming. The clock was ticking. And the nightmare scenario was clear: turn up on day one and spend the morning buried in paper booking sheets, manually cross-checking payments, and fielding fifteen phone calls about whether there's space for a Saturday afternoon session.
They needed more than a website. They needed software that ran their business.
Why off-the-shelf didn't work
There are plenty of booking systems you can subscribe to. The problem is they all make assumptions about how your business works — and Cloud Nine didn't fit any of them:
- Five different session types (standard play, baby social, childminder, SEN, exclusive hire) — each with its own capacity rules
- Birthday party bookings on a completely different flow: deposit payments, themes, extras, different rules
- Adults are free. Only children are charged — with pricing that changes by age bracket, session type, and date-specific overrides
- Date-level blocks on individual sessions (not full-day closures — just "no baby social this Tuesday")
- Staff who need to handle bookings, cancellations, and refunds without access to anything dangerous
A SaaS booking platform would handle maybe two of those. The rest would be workarounds and spreadsheets. That's not a system — that's a compromise.
"Rob at Blue Penguin designed us the most amazing, customer friendly website I could have asked for. My business would not run without it."
— Ellie, Owner
What we actually built
A custom Laravel 12 platform on PHP 8.2, with a MySQL database, Stripe payment processing, and an admin panel that staff can use from a laptop at the cafe counter.
Here's what it does every day:
What happens on the customer side:
- Browse available sessions, select dates and times, register children with ages
- Pay by card through Stripe Checkout
- Manage bookings, cancel with automatic refund calculation (7-day deadline from booking date)
- Receive confirmation emails and, if applicable, birthday reminders
What the admin panel does:
- Dashboard showing upcoming bookings, revenue, and session capacity
- Create, edit, and cancel bookings on behalf of customers (manual payments — cash, card, or phone)
- Override pricing for specific date ranges and session types
- Manage play sessions — set times, capacities, block specific dates
- Role-gated access: staff accounts can manage bookings without touching sensitive back-office settings
And the invisible bits nobody sees:
- Stripe webhook verification ensures every payment produces exactly one booking — even when confirmation arrives from multiple paths simultaneously
- Booking table indexes on date, session, and status keep queries fast as the dataset grows
- Pricing rules cached with automatic invalidation — capacity calculations count confirmed bookings only
- Database queue for email delivery and scheduled tasks (birthday emails go out every Monday at 9am, automatically)
The hard bit wasn't taking payments
Stripe handles the card stuff. That's the easy part.
The hard part was making sure one payment became one booking, every time — even though confirmation could arrive from two completely different paths: the Stripe webhook callback AND the customer's browser return from the checkout page. If you don't guard both entry points, you get duplicate bookings. Same payment, two reservations, one very confused parent at reception.
The fix wasn't clever. It was careful. Metadata written at checkout creation. Ownership checks during confirmation. Duplicate protection keyed against Stripe payment IDs at the service layer — not the controller — so every path to "paid" goes through the same gate.
Why it runs fast
PageSpeed Insights scores for the booking page (where the real work happens):
- Mobile: Performance 100/100. LCP 1.0s. CLS 0. All Core Web Vitals pass.
- Desktop: Performance 100/100. LCP 0.3s. CLS 0.
A typical WordPress site with a booking plugin ships 800KB+ of JavaScript before you've configured the first session type. This entire platform serves less than half that — and most of it is cached.
The secret isn't complicated. Start with a clean base. Add only what you actually need. Don't run a CMS runtime and a chain of plugins just to take a booking.
The tech stack (for the curious)
- Framework: Laravel 12, PHP 8.2
- Database: MySQL 8
- Frontend: Tailwind CSS, Alpine.js, Vite
- Payments: Stripe Checkout with webhook verification
- Queue: Database driver with scheduled tasks via Laravel's scheduler
- Auth: Session-based with role middleware and password-change enforcement for admin accounts
- Database design: 35 migrations tracking schema evolution from initial build through pricing overrides, birthday emails, and manual booking support
What this means if you're in the same boat
If your business needs more than a brochure site — bookings, customer accounts, payments, workflows — you have two options:
- Subscribe to a SaaS platform and adapt your business to fit its assumptions
- Build something that works the way your business actually works
Option 1 is cheaper up front. Option 2 is cheaper over time — because you own it, you're not paying monthly forever, and you're not building your operations around someone else's product roadmap.
We do option 2. Not because it's easier. Because it's right.