SNAP is a self-photo studio chain across six Indonesian cities. The old site was a static brochure — bookings happened in WhatsApp DMs, every branch ran its own notebook, and roughly one in twelve sessions ended in a clash at the door. We rebuilt the customer flow as a PWA and shipped a single admin dashboard that sees all six branches at once.
A mobile-first PWA on the customer side, a desktop SPA on the admin side, one API and one database underneath. Six modules carry the whole chain — from the city splash a customer sees on Instagram to the daily payout a branch manager confirms at close.
Six cities, twelve branches, two physical studio archetypes. Customer flow auto-skips the branch step in single-studio cities and routes the right capacity model based on the venue.
Box studios run a backdrop×time matrix — each room books independently. Roll-down studios run one shared schedule with a pure-aesthetic backdrop pick. Same UI shell, two different capacity rules underneath.
Customer scans a dynamic QRIS code, the merchant callback releases the slot, the customer sees a checkmark. No staff watching m-banking. No manual "is it paid yet?" message.
One dashboard for the founder. Today's bookings across six branches, sales vs. yesterday, expenses by category, occupancy by room. Drill from any tile into the raw rows.
Branch managers see only their branch — today's calendar, walk-in slot toggle, expense logger, end-of-day cash count. Role-based, locked by default.
Booking confirmed → templated WhatsApp goes out with the slot, branch address and a calendar link. Reminder fires 2 hours before. The DM thread is no longer where bookings live, but it's still where customers expect to hear back.
Every branch had its own admin number. Customers messaged "tomorrow 3pm Kemang?" and someone replied if they were awake. Two staff sometimes confirmed the same slot to two different customers because the inbox was the only source of truth.
On busy weekends roughly one session in twelve ended in a clash at the door — one party got bumped, refunded, and angry on Instagram.
Customer picks a city, a branch, a date, a time and a backdrop — each step writes a hold on a real row. Two phones tapping the same slot at the same second: one wins, the other sees "just taken" and is offered the next free 30-minute block.
Confirmation goes out as WhatsApp afterwards, so customers still hear back where they expect. The DM is the receipt now — not the booking.
Half the branches are box studios — each backdrop sits in its own room, so two backdrops means two parallel sessions. The other half are roll-down studios — multiple backdrops hung in one room, but only one session can run at a time.
The legacy site treated them identically. Customers booked four backdrops at the same time at a roll-down branch and showed up to find they couldn't actually all happen.
Each branch is tagged multi or single in the database. Box studios get a backdrop×time grid — pick a cell and you've picked a room and a time in one move. Roll-down studios get one shared schedule and a separate backdrop pick that's purely aesthetic.
Same React shell, same components, different data shape from one endpoint. The customer never has to know the difference.
○ free × taken ● picked
Customer transfers, screenshots the proof, sends it on WhatsApp. Branch admin opens the m-banking app, hunts for the matching amount, replies "ok confirmed" and writes the slot in the notebook. Five minutes per booking on a quiet day, much longer on a Saturday.
If the admin was on lunch, the customer waited. If the customer waited too long, they cancelled.
Confirms automatically.
No screenshot needed.
For one branch admin on a Saturday between opening and noon — the busiest stretch. Same person, same job, before and after the rebuild.
Sat with two branch admins for a full Saturday each. Watched the notebook get filled, the chats get juggled, the m-banking app get refreshed. Wrote down every step. Cut anything that didn't have to exist if a database existed.
React + Vite PWA, Express + MySQL on the back. Two booking models behind one endpoint shape so the UI didn't fork. QRIS merchant integration with webhook fallback so a flaky network never strands a customer mid-payment.
Admin SPA: overview, bookings calendar, expenses, sales, reports, settings. Rolled out one branch first (Kemang), watched it for a week, then onboarded the rest two at a time. Notebooks retired branch by branch.
Numbers below are from the live database for the pilot branch (Kemang) plus the two branches that came online after. Chain-wide figures get published here only once there are enough months of real data behind them.
Six branches across six cities, a founder who has to be able to read the dashboard on a phone in line at a coffee shop. Every decision answered: what happens when the wi-fi at one branch drops mid-Saturday-rush?
Installable from the homescreen. Page transitions via Framer Motion. CSS Modules and a strict black-and-white palette — the photos do the colour, the chrome stays out of the way.
One database, branches as rows, capacity model as an enum. Slot writes go through a single endpoint that locks the row to keep two simultaneous bookings from winning the same cell.
Per-booking dynamic QRIS code with a 15-minute TTL. Settlement webhook releases the slot; if the webhook misfires, a poller reconciles within a minute. Either path, the customer sees the same checkmark.
No separate admin codebase. Founder login lands in the desktop sidebar layout; branch login lands in a single-branch view. RBAC at the API, not the UI — clicking around can never grant you something the server hasn't already approved.
Free scope-out call. We'll tell you the cheapest thing that fixes it.
Builds like this run on the Core Platform + custom modules — most booking systems land between $2,500 and $4,500. Full pricing →