White-label staff app, second group
The staff platform rebuilt and shipped for a different pub group — two venues, its own database, its own App Store listing. Proof the first one was a product rather than a one-off: 41 Swift files, 19 migrations, approved by Apple as an unlisted app. It has since grown to a third venue, a members’ club system and a web portal, so staff without an iPhone are covered too.
What I built
- 41 Swift files, around 15,000 lines, 19 numbered migrations, across two venues with separate checklists, draught lines and staff.
- Geofenced clock-in with the live distance shown on screen, breaks recorded separately, and a rota builder with draft-then-publish so staff only see finished weeks.
- Holiday accrued live at the UK statutory 12.07% of hours worked for hourly staff, calculated from completed timesheets so it is never stale, on a 1 April holiday year.
- Peer-to-peer shift swaps with an admin gate — only swaps both parties have agreed reach the inbox, and approving reassigns the shifts in the database rather than leaving someone to edit the rota by hand.
- End-of-day sheets, income by category and cash-ups, with variance shown green, amber under £5 and red at £5 or more.
- Documents in private storage served through signed URLs valid for one hour.
The cellar ordering model
Every draught line carries a par level — what should be on site immediately after the weekly delivery. Count the cellar and the order is simply par minus counted.
Par is derived from sales rather than guessed: a week of average demand plus a safety margin of 1.65 standard deviations, which sets a 95% service level — the line runs dry no more than once in twenty weeks. Sellable pints allow around 7% loss to line cleaning and ullage.
Since launch: v1.3 to v1.7
- A mobile web portal on the same database, so Android staff get rota, checklists, closing, stock counts, temperature logs and a nightly cash count that syncs with the app. Cash totals add themselves up, and any till discrepancy is recorded but kept out of the total.
- A members’ club system: 189 existing members imported from a spreadsheet, random member codes that can’t be guessed, QR cards scanned at the door, a printable card export, and function room bookings.
- Push notifications for shift reminders and new bookings, the one feature missing at the first handover.
- Toilet-check grids with manager sign-off, weekly compliance reporting, and managers able to edit shifts and timesheets from the portal, with a mandatory reason and a full audit log on every change.
The security pass
It started with two findings. An admin PIN was still the default, and the server role had never been granted table privileges, so every server function had been failing silently for months.
What went wrong
- A full outage during the build, caused by one missing clause. Row-level security policies created without an explicit role clause don’t apply to the role the app actually uses, so every query failed with a permission error — which the login screen reported to staff as “Invalid PIN”. The symptom pointed nowhere near the cause.
- Daily checklists weren’t resetting overnight. The view was merging today’s completions and this week’s into one lookup, so a daily item ticked on Monday still read as done on Tuesday. The data was never wrong — only the display.
- Two Supabase projects with near-identical names in the same dashboard; running SQL against the wrong one fails silently with “relation does not exist”. Documented in the handover as the single most likely mistake for whoever comes next.
Handed over honestly
- One venue’s GPS coordinates are an estimate that was never verified on site — written down, with the on-screen distance reading given as the way to check it.
- One account still on a placeholder PIN, and a possible duplicate staff record, both flagged rather than left to be discovered.
- Push notifications scoped and not delivered — the only substantial feature promised and not built. CSV export, reports and photo attachments were considered and deliberately dropped.
- Security rests on PIN-gating in the app rather than per-user policies. Stated as a deliberate trade-off for a small team, with the cost of changing it spelled out rather than presented as a finished security model.