Case Study · POS & Retail

Built for a multi-location salon group.Client name withheld under NDA.

  • POS & Retail
  • Multi-site operator
  • End-to-end build

POS App Development Case Study: How We Built a Role-Based System for a Multi-Location Salon Group

A multi-location salon group runs appointments, sales and reporting for every location from one Flutter POS app, with roles enforced at the data layer and offline-first operation.

Illustrative render of the salon POS app: a dashboard on a tablet showing today's revenue, active appointments, a QR booking-link card, service breakdown and quick actions, surrounded by cards for today's appointments, location switching between three salons, checkout, and offline transactions queued and synced
Industry
POS & Retail
Engagement
End-to-end build
Platforms
iOS & Android (Flutter)
Our role
Product, build, data model, release
Scale
Multi-site operator
Overview

One App for Every Salon and Every Role

This POS app development case study covers a multi-location salon group that needed one app to run appointments, services, products and sales across all of its locations. We built a single Flutter app where an owner, a supervisor, and a cashier see three genuinely different products, with permissions enforced at the data layer rather than the interface, and transactions that keep working through a network outage.

The Brief

One App, Every Location

The group wanted appointments, services, products and sales for every salon in one mobile app. The questions an owner cares about span locations, such as which services bring in the most revenue, which staff member is busiest and where, or whether this week is ahead of last week. The app had to answer those for the owner while keeping each supervisor and cashier inside the salons they work in. The real brief was in what "one app" needed to mean.

The Problem

Same App, Three Different Operating Realities

An owner needs visibility across every location at once. A supervisor needs full control of the one or more salons assigned to them and zero visibility into any other. A cashier needs to move through a sale quickly and must only ever see the salon they are assigned to.

The common shortcut is one interface with controls hidden per role. It looks fine in a demo and fails in production: the data still reaches the device, the permission logic lives in the UI layer where a future refactor can quietly break it, and every new feature is a fresh chance to leak one location's numbers onto another's screen.

Two more constraints shaped the build: bookings made through the group's website had to reach the app with no manual re-entry, and the app had to keep taking payments through a network outage. A POS that stops selling on a busy Saturday because the router rebooted has not been given a solution, it has been given a liability.

What We Built

Four Load-Bearing Decisions

1

Role-based architecture with per-salon isolation

Three access levels, enforced by role on the database side in Supabase rather than in UI logic. Owners see every salon, supervisors see only the salons assigned to them, and cashiers see only the salon they are assigned to. A cashier's queries physically cannot return another location's records. The isolation lives in the backend, so it can't be undone by a future UI change.

2

A QR bridge between web bookings and the till

The app generates a unique QR code that opens the salon's web booking page, so walk-in or scheduled customers book from their own phone and the appointment reaches the app without staff re-typing it. No manual lookup, no transcription errors during rush hour.

3

Owner-readable reporting, no portal required

Sales graphs update live as transactions close. Owners export revenue by salon, staff, service or period as a PDF or Excel file generated directly on the device.

4

Offline-first by design, not as a patch

Data is cached on the device with SharedPreferences; transactions queue during outages and sync automatically on reconnect. This was an architecture decision from day one, not a resilience feature added later.

The Decision

Enforce Roles in the Data Layer, Not the Interface

The default approach

Build one screen set, hide controls per role. Fast, common, and looks correct in a demo.

Why it fails here

Hiding a control doesn't stop the data reaching the device. In a multi-location business, that's not theoretical. One salon's takings could be pulled from a device sitting on another salon's counter. It also decays over time, since permission logic sitting in the UI means every future screen is a fresh chance to reintroduce the gap, silently.

What we did instead

Designed the data model, covering roles, scopes and isolation, before building a single screen, and enforced it as backend rules. The UI just reflects what the query returns.

What that bought

A new screen can't introduce a permission bug, because screens no longer hold permission logic. The security property belongs to the data, and holds regardless of who builds the next feature.

Under the Hood

Tech Stack & Architectural Choices

Flutter
One codebase, both app stores, one team
Supabase (PostgreSQL)
Relational data model with role and salon scoping enforced at the data layer
Firebase Analytics & Crashlytics
Usage insight and production crash reporting
syncfusion_charts
Live sales graphs updating as transactions land
flutter_barcode
Generates the QR codes that link customers to web booking
pdf / excel
Owner-readable reports generated on-device
SharedPreferences
On-device cache that keeps the till working through an outage
Provider
State management sized to the app, with no unnecessary ceremony
The Outcome

Build Facts & Delivery Results

The app shipped to both the App Store and Google Play. Owners see cross-location performance on one screen, supervisors run the salons assigned to them independently, and cashiers complete a transaction in fewer steps with fewer opportunities to get it wrong.

3
roles enforced at the data layer, not the UI
0
paths for a supervisor or cashier to reach a salon they aren’t assigned to
2
app stores served from one codebase
100%
of transactions queue and sync automatically when the network drops

Build facts, verified against the delivered app. No outcome numbers are published unless they were actually measured.

Evidence

What You Can Check Yourself

Where each claim on this page comes from

4.9/5 across 5 verified client reviews on Clutch
✓ Publicly verifiable
100% Job Success over 49 completed jobs on Upwork
✓ Publicly verifiable
Shipped to both app stores under client's developer account
From delivery record
Revenue or transaction-volume change post-rollout
Not recorded
Why It Matters

Architecting Systems That Stay Correct Over Time

Multi-tenant apps, where one product serves users who need genuinely different views of the same data, are harder than they look, and the difficulty is never in the screens. It's in deciding where permission logic 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 as other people extend it, and can be handed to a new developer without becoming a security review. This is the same approach we bring to hospitality, retail, field services, or any multi-location build.

FAQ

Frequently Asked Questions

How long does it take to build a role-based POS app like this with Flutter?

Depends on scope, but a multi-role, offline-first POS with backend permission enforcement typically runs several months for a full end-to-end build, from data modeling through app store release.

What does a multi-location retail app with role-based access cost?

Cost is driven mainly by the number of access levels, integrations (like QR/booking bridges), and offline-sync requirements, not by screen count. Get a scoped estimate based on your actual role structure rather than a flat industry number.

Why enforce permissions in the database instead of the app's UI?

Because UI-level permission hiding doesn't stop data from reaching the device. It only hides it visually. Data-layer enforcement means a permission bug can't be introduced by a future screen or refactor.

Does the app work if the internet goes down mid-sale?

Yes. Transactions queue locally and sync automatically once connectivity returns, so a network outage doesn't stop sales.

Building something where roles actually matter?

Bring the awkward parts of your requirements; those are the useful ones.