Skip to content

A staffing platform that keeps its rules in the database

Recruitment, right-to-work, rostering, check-in, pay and client paperwork for a large pool of zero-hours workers: three front ends over one Postgres database that refuses to break its own rules. Built and tested on staging, awaiting the client's content and keys.

A UK event-staffing agencyClient engagement2026On staging
The back-office event board: confirmed and invited bar staff, and a ranked pool of candidates scored on show-rate, rating and distance
The product itself, running with demonstration data.
front ends over one database
3
database assertions in CI
5,000+
migrations behind one schema
200+

The problem

A hospitality agency runs many events a day in season, from a large pool of zero-hours workers. Every one of those shifts depends on rules that are easy to state and easy to get wrong at volume: is this person allowed to work, have they hit their weekly cap, did they travel from another venue in time, what are they owed once the break is taken off.

The work was spread across a legacy system and manual follow-up. Onboarding was chased by hand, right-to-work checks were a person with a browser and a spreadsheet, and the client-facing paperwork was assembled after the event rather than produced by it.

Three groups of people use the result (office staff, workers on their phones, and the hotels and venues that hire them), and one of those groups must never see what a worker is paid or anything personal about them. That is a database problem before it is an interface one.

What we built

01

Three apps, one database, and rules that cannot be bypassed

A back-office portal, an installable staff app and a read-only client portal all sit on the same Supabase Postgres. The business rules are not in the front ends: they are Postgres functions, triggers and policies, so a bug in any of the three apps cannot let a rule through. State machines are enforced twice: in the domain code and again by a trigger that rejects illegal transitions, and every edge is asserted by a database test.

02

A client portal that can never read money

The client role has no table policy at all. It reads only through owner-rights views, and each view filters itself to what that client is entitled to see. A test pins the exact set of policies, views and privileged functions, so adding one without review fails the build, and a second check fails if an app writes to an admin-only table directly. 'The client cannot see pay' is a property the pipeline proves on every merge, not a promise in the spec.

03

Auto-assign with hard gates before any scoring

Filling an event runs hard gates first (role, language or gender requirement, blocked workers, double-bookings with a two-hour gap between venues, the weekly hours cap, do-not-return lists) and only then scores what remains: show-up rate, rating, proximity, fair rotation and venue history, with the weights editable in settings. Workers qualified for that client are exhausted before anyone else is considered, and that is an ordering, not a weight, by design.

04

Hours and pay computed, never stored, and written twice

The weekly cap depends on term time, student-visa limits, a completion-letter date and the 48-hour opt-out, so it is calculated on demand rather than kept as a number that can go stale. Pay is the overlap of check-in to check-out with the booked shift, with a grace period, a minimum paid length and unpaid breaks removed. Both the cap and the pay rules exist in TypeScript and in SQL, and both are held to the same JSON test vectors so the two cannot drift apart.

05

Time rules as data, and a clock that knows it is on UK time

Deadlines such as the 12:05 cut-off are rows in a schedule table, run by pg_cron in UTC and gated on UK time inside the function, so the rule survives the clocks changing. Notifications go through an outbox with unique keys and row locking, so a retry never sends twice, and copy lives in a register as data. Anything not yet configured waits in the queue rather than failing quietly.

06

Automation that proposes and a person decides

The Home Office right-to-work check is driven by a headless browser that stores the result and the photograph, and it never marks anyone verified: an administrator compares the photo with the worker's selfie and presses Verify or Reject. Document reading by a language model follows the same line. It pre-fills dates with a confidence score, flags doubtful reads, never overwrites what the worker typed and never verifies. Both are built and switched off until the client has signed off the legal and data-processing position.

07

Erasure that cannot be lost, and a bug worth keeping

Removing a worker anonymises them while keeping the history and the paperwork already issued, and queues storage deletion so an erasure request is never dropped. And in testing, every scheduled job returned 401 because the key stored in the vault and the key the function held differed byte for byte while both were valid. The fix was to confirm the key with Supabase Auth instead of comparing strings, and the test that pins it came from the failure.

The client portal listing events with confirmed headcount per role and timesheet downloads
The client portal listing events with confirmed headcount per role and timesheet downloads

The outcome

  • A complete platform (onboarding, compliance, rostering, check-in, pay, reporting and client paperwork) running on a staging environment with sample data, ready for the client to walk through. It is not live with real workers: email, push and the scheduled jobs wait on the client's keys, sender domain, wording and sign-offs.

  • The rules that matter most are checked in the database, not trusted to the interface: the client portal's blindness to money, the hours cap, the right-to-work hard stop and the state machines all have tests that fail the build.

  • The test suite is large for the size of the project (thousands of unit assertions, thousands of database assertions and an end-to-end layer), and it is what made it practical to build the whole thing with AI agents working in parallel, each on its own branch.

  • What is deliberately not claimed: no usage numbers, no time saved, and no live production traffic. The remaining work is documented as a list of the client's decisions and inputs, not hidden in the code.

Built with

  • Next.js
  • TypeScript
  • Supabase
  • PostgreSQL
  • PostGIS
  • Row-level security
  • pg_cron
  • Edge Functions
  • Web Push
  • Resend
  • Playwright
  • Vercel

Page copy drafted with AI assistance. Skofi is accountable for it. Editorial policy

Start here

Have something shaped like this?

If any part of the above resembles a problem you are living with, a short call is the fastest way to find out whether it is the same problem underneath.

Book a 15-minute callsaveez@skofi.tech

What happens on the call

  • 15 minutes, no deck
  • We map the process on the call
  • A written scope, or an honest no
Book a call