Whitepaper · Internal Staff Platform

One platform, five tools retired.

Engineering record for a bespoke internal staff platform delivered to an anonymised UK fashion retail client. Attendance, leave, expenses, timesheets, and a tablet kiosk — consolidated onto one Postgres, one auth boundary, and one deploy.

Next.js 16SupabaseRLSReceipt OCRPWA KioskResend
5 → 1tools consolidated
~32xfaster monthly payroll
<2sreceipt OCR latency
95%+OCR field accuracy

Executive summary

An anonymised UK fashion retail client of roughly 50 staff was running their workforce across five disjoint systems: an attendance spreadsheet, a leave SaaS, a paper expense process, a timesheet tool, and a clipboard at the door. We replaced the lot with a single Next.js 16 application backed by Supabase Postgres, with row-level security as the primary authorisation layer, an OCR-driven expense pipeline, a tablet PWA kiosk, and a one-click monthly payroll export. Payroll reconciliation went from ~16 hours to ~90 minutes. Four paid SaaS subscriptions were cancelled.

01Background

The client is a UK-based independent retailer with a head office buying team plus a retail floor. Across both, around 50 staff were tracked across five tools that had accreted over the years: a OneDrive attendance workbook, a SaaS leave tracker on the cheapest tier, a shared inbox of phone-photographed receipts, a separate timesheet web app for the buyers, and a paper sign-in book at reception that the team forgot to update.

The presenting symptom was payroll. Each month a manager spent two working days reconciling the four written sources against one another to produce a payroll input. The deeper symptom was that nobody could answer simple questions: who is in today, how many days off has somebody got left, what did the team spend on lunches this quarter. The data existed; it was scattered and stale.

02The problem in detail

SymptomPre-platform measurement
Time spent reconciling monthly payroll~16 hours per month, by a senior manager
Average expense submission to approval~9 days
Receipts lost between phone and approval~12 percent of submitted spend
Kiosk sign-in adherence~70 percent on a typical week
SaaS subscriptions in scope4 paid + 1 spreadsheet

The combined SaaS spend was modest in isolation but compounding. More importantly, none of the tools talked to one another. Manual reconciliation was the integration layer.

03Goals and non-goals

In scope

  • Five integrated modules: attendance, leave, expenses, timesheets, kiosk sign-in
  • Phone-camera receipt capture with OCR-extracted merchant, date, total, currency, and category guess
  • Manager approval flows with email + in-app notification
  • Tablet kiosk (PWA) for the front door, with PIN sign-in and offline tolerance
  • One-click monthly payroll export (CSV) reconciled across all modules
  • Director weekly digest email summarising anomalies
  • Role-based access for staff, managers, directors, admins, enforced at the database layer

Explicitly out of scope

  • Recruiting and ATS. The team is small enough that hiring is bespoke. Out.
  • Performance reviews. Sensitive, slow-moving, and the client did not want to digitise it yet.
  • Direct payroll filing. The export feeds the accountant’s pipeline. We do not touch HMRC submission.
  • Native mobile apps. A responsive PWA was sufficient. Native would have 5x’d the cost.

04Architecture

┌──────────────────────────────────────────────────────────────┐
│  Phone (employee)   Tablet (kiosk)   Desktop (manager/admin)  │
│       │                  │                  │                 │
│       └─── Supabase Auth cookie / kiosk service-token ────────│
│            ↓                                                  │
│  Vercel Edge → Next.js 16 (App Router)                        │
│            ↓                                                  │
│  Server Actions (typed, end-to-end)                           │
│       │                                                       │
│       ├─→ Postgres 15 (Supabase) — RLS enforced               │
│       │     attendance, leave, expenses, timesheets, kiosk    │
│       │                                                       │
│       ├─→ Supabase Storage (private bucket) — receipt images  │
│       │                                                       │
│       ├─→ OCR worker → Anthropic SDK → claude-sonnet          │
│       │     extracts merchant, date, total, currency, category│
│       │                                                       │
│       └─→ Resend (transactional email)                        │
│                                                               │
│  Vercel Cron                                                  │
│   • 06:00 daily  → overdue approval reminders                 │
│   • 07:00 Monday → director weekly digest                     │
│   • 23:55 last-of-month → payroll snapshot                    │
└──────────────────────────────────────────────────────────────┘

Module data model

  • users, profiles, roles — auth + role assignment
  • attendance_punches — clock-in/out events; one row per punch
  • leave_requests, leave_balances, leave_types — UK statutory leave model with bank holidays per region
  • expenses, expense_attachments — one expense, one or more receipt images
  • timesheet_entries — buyer / back-office tracking against projects
  • kiosk_punches — separate table from attendance for retail floor sign-in
  • payroll_snapshots — frozen end-of-month aggregates for audit

05Key technical decisions

Single database over five integrations

Every module writes into the same Postgres. Cross-module features (payroll export, weekly digest, anomaly detection) are SQL joins, not API integrations. This is the single biggest architectural lever the platform has. Five SaaS tools talking via webhook hub is at least 5x more code, 5x more failure modes, and pays a vendor tax forever.

RLS as the authorisation layer

Four roles (staff, manager, director, admin) and seven tables means a 28-cell policy grid. Encoding it as RLS keeps the truth next to the data:

CREATE POLICY "manager_sees_direct_reports_expenses"
  ON expenses FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM profiles p
      WHERE p.user_id = expenses.user_id
        AND p.manager_id = auth.uid()
    )
  );

The application code does not check permissions; it queries and trusts that Postgres returns only the rows this user is entitled to see. Service-role access is used only by trusted server-side cron handlers.

Receipt OCR via Anthropic SDK

Receipts are messy: faded thermal paper, multiple currencies, foreign characters, hand-written totals, photos taken at angles. We tried two open-source OCR pipelines first; both struggled on real-world receipts. Claude Sonnet via the Anthropic SDK consistently extracted merchant, ISO date, total, currency, and a category guess at 95 percent+ field accuracy on a 200-receipt evaluation set. Latency under 2 seconds. The cost per receipt is fractions of a penny at the client’s volume.

The OCR call returns strict JSON validated by a Zod schema. If the schema fails, the user sees an empty form and types it themselves. There is no “maybe right” data path.

PWA kiosk over native app

The tablet at reception runs a single browser tab in fullscreen, installed as a PWA. A service worker caches the kiosk bundle so a flaky wi-fi connection does not break sign-in. Punches are queued locally and replayed on reconnect. The kiosk uses a separate scoped service token, not user auth, with a tightly bounded RLS policy that allows only kiosk_punches inserts.

Vercel + Supabase over self-hosted

The client wanted no server administration. Vercel handles deploy, TLS, scaling, cron, logs. Supabase handles Postgres, auth, storage, realtime. The combined operational surface is roughly two dashboards and a status page subscription. Self-hosting would have been cheaper in raw infra cost and dramatically more expensive in attention.

Monolith over microservices

One Next.js app, one repo, one deployment. There is no scenario at this scale where splitting attendance from expenses into separate services pays back. The team is one engineer; the database is shared; the operations are coupled. A monolith with strong module boundaries is the right tool.

06Implementation milestones

PhaseWeeksDeliverable
Discovery1Workflow shadowing, role matrix, module boundaries, data model draft
Foundations2-3Auth, profiles, roles, RLS policies, deployment pipeline, design system
Attendance + leave4-5Both modules end-to-end, manager approvals, balance accrual, statutory leave defaults
Expenses6-7Receipt upload, OCR pipeline, approval flow, manager email + approve link
Timesheets + kiosk8-9Buyer timesheets, kiosk PWA, offline queue, PIN sign-in
Payroll + analytics10Monthly snapshot, CSV export, weekly director digest cron
UAT + cut-over11-12Parallel run with old tools for one cycle, fix list, sunset of old SaaS

07Results

Adoption was the unusual win. The platform was never made mandatory. Staff used it because it was faster than the alternatives. The kiosk became the default sign-in within a week. The phone-camera expense flow had near-100 percent adoption inside a fortnight because typing receipt details into a spreadsheet was just worse.

  • Payroll reconciliation: 16 hours/month → 90 minutes/month (32x improvement)
  • Expense submission to approval: 9 days → ~1 day
  • Lost receipts: ~12 percent → near zero
  • Kiosk adherence: ~70 percent → ~98 percent
  • Subscriptions cancelled: 4 paid SaaS

The platform paid back its build cost inside the first year on saved licences and saved admin time alone, before counting the second-order effects (faster decisions, fewer disputes, better visibility).

08Lessons learned

One database is the cheat code

Cross-module features are trivial when every module shares storage. Five SaaS integrations are five hard problems. One Postgres is one easy one.

RLS is documentation

A future maintainer can read the policies and immediately understand who can do what. That is far better than auditing thirty route handlers for permission checks.

Strict JSON schemas keep OCR honest

The OCR call returns JSON; if it fails Zod parsing, we discard it and ask the user. There is no “sort of right” path. That single rule eliminated an entire class of subtle data quality bugs.

Adoption is a UX problem

No formal training was delivered. The platform replaced the old tools because it was faster. If you have to teach people to use your software, your software is too hard.

Build for the boring path

The unsexy modules (attendance, payroll export) get used every day. The fancy AI bit is the demo. Get the boring path airtight first.

09Conclusion

A bounded, opinionated, monolithic stack delivered five integrated modules in roughly twelve weeks, replaced four paid SaaS tools, and recovered a senior manager’s monthly payroll day. The interesting engineering was in the boring decisions: one database, RLS-first auth, strict OCR schemas, a PWA kiosk. None of these are novel; that is the point. The leverage came from picking conventional tools with discipline and resisting the temptation to split, micro-service, or re-platform.