Built for a multi-location salon group. Client named on request, under NDA.
Appointments lived in a paper book, sales in a till, and staff hours in a spreadsheet — a separate set of each, per salon. We replaced all of it with one Flutter app where an owner, a supervisor and a cashier see three genuinely different products, and where losing the internet mid-shift doesn’t stop a sale.
A multi-location salon group was running its business on three separate systems. Appointments lived in a paper book at the front desk. Sales went through a standalone till. Staff hours sat in a spreadsheet someone updated at the end of the week. Each salon kept its own copy of all three.
That arrangement holds together until you try to answer a question that spans locations. Which services actually make money? Which staff member is busiest, and at which site? Is this Tuesday better or worse than last Tuesday? Every one of those questions required somebody to sit down with three sources of truth and reconcile them by hand — which meant, in practice, that they went unasked.
They wanted one mobile app for the whole operation. The interesting part of the brief was what “one app” had to mean.
An owner needs to see everything, everywhere, at once. A supervisor needs full control of their own salon and no visibility into any other. A cashier needs to move through a transaction quickly and accurately, and must not be able to reach the payroll summary at all.
That is the part teams get wrong. The tempting approach is to build one interface and hide the controls each role shouldn’t see. It demos beautifully and it is structurally unsound: the data is still on the device, the permission logic lives in the widget tree where any refactor can quietly undo it, and every new feature is a fresh chance to leak one location’s numbers onto another’s screen.
Two further constraints shaped the build. Appointments booked through the group’s web presence had to arrive in the mobile app without anyone retyping them. And the app had to keep taking payments through a network outage — a salon that stops selling on a busy Saturday because the router rebooted has not been given a solution, it has been given a liability.
Three access levels — owner, supervisor, cashier — enforced through role-scoped Firestore rules rather than in the UI. A cashier signing in at one salon cannot address another site’s records at all, because the query never returns them. The isolation is a property of the backend, so a future feature cannot accidentally opt out of it.
Customers book online; the app generates and reads a QR code that resolves a booking to the right service and staff member at check-in. The cashier scans, and the transaction opens pre-filled. No lookup, no re-entry, no transcription error at the busiest moment of the day.
Sales graphs update as transactions complete. At any point an owner or supervisor can export a full report — revenue by service, by staff member, or by period — as a formatted PDF or Excel file, generated on the device and shareable straight from it. No web portal to log into, no export queue.
Critical data is cached locally and transactions queue when connectivity drops, syncing automatically when it returns. This was treated as a hard requirement from the first architecture conversation rather than a resilience feature bolted on near the end — which is why it works.
Build one screen set, then show and hide controls per role. It is fast, it is what most multi-role apps do, and it looks correct in a demo.
Why it fails hereHiding a control does not stop the data reaching the device. In a multi-tenant business that is not a theoretical concern — it means one salon’s takings are retrievable from a device sitting on another salon’s counter. It also degrades over time: the permission rules live in the widget tree, so every refactor and every new screen is another chance to reintroduce a gap, and nothing fails loudly when someone does.
What we didWe designed the data model first — roles, scopes and isolation rules established before a single screen existed — and expressed them as role-scoped rules on the backend. The UI then reflects what the query returns, rather than deciding what the user deserves to see.
What it boughtAdding a screen cannot introduce a permission bug, because screens no longer hold permissions. The security property is a property of the data, and it holds whoever writes the next feature.
The app shipped to both the App Store and Google Play. Owners see cross-location performance on one screen, supervisors run their own site independently, and cashiers complete a transaction in fewer steps with fewer opportunities to get it wrong. The reporting that previously required someone to reconcile three systems by hand is now an export button.
Build facts, verified against the delivered app. We don’t publish outcome numbers we didn’t measure.
Some of this engagement stays private. Naming what’s missing seems fairer than quietly leaving gaps.
Multi-tenant apps — one product serving users who need genuinely different views of the same data — are harder than they look, and the difficulty is almost never in the screens. It is in deciding where permission lives. Put it in the interface and you have a demo. Put it in the data layer and you have a system that stays correct while other people extend it.
We design the data model first: roles, scopes and isolation rules before any screen exists. If you’re building for hospitality, retail, field services, or multi-location anything, that’s the approach we bring — and it’s the reason this app can be handed to a new developer without becoming a security review.
We’ll walk you through this build in detail on a call — screens included, under NDA. Bring the awkward parts of your requirements; those are the useful ones.
More proof: all case studies · the engineering blog · what clients say