StaffClock
Live · clock.mindvisionstudio.comInternal restaurant web app — fast clock-in, estimated salaries, and automatic card-tip distribution (front/kitchen pro-rata to hours, exact to the cent). Pivoted into a multi-tenant SaaS: super-admin → establishments → admins, strict isolation, PIN-based tablet Consultation account, employee view, full audit log. Cloudflare edge (Workers + D1 + R2 + Hono), React 19, France GDPR. Deployed to production.
Why I built it
A restaurant was tracking hours on a slow Android app — the round time picker, impossible to use quickly during a rush. Card tips were split by hand, salaries estimated by gut feel. I built StaffClock for that exact need: big-button clock-in, automatic front-of-house tip distribution to the cent, estimated salaries, formal closures. Once the tool was solid for one restaurant, I pivoted it into a real multi-tenant SaaS — super-admin → establishments → admins by invitation — so any establishment can have its own, isolated from the rest. All on Cloudflare edge (Workers + D1 + R2), ~$5/mo, deployed to production at clock.mindvisionstudio.com.
Structural technical decisions
The single-restaurant → multi-tenant SaaS pivot added an establishment_id column on every table and a TenantContext carried by the session, never the URL. The per-establishment slug (`/<slug>/…`) is purely cosmetic: login is global and the server never trusts it to decide the tenant. Super-admin with impersonation ("Enter" an establishment → "Leave" banner). Isolation was validated by an adversarial audit of 8 agents hunting for a cross-establishment leak: zero leaks found.
Front-of-house distribution is pro-rata to hours worked, but the sum of the shares must land exactly on the entered total — no rounding that creates or destroys a cent. Implemented via the largest-remainder method: compute integer shares in cents, then distribute leftover cents to the largest remainders. Day / Week / Month views, and no amount visible in read-only consultation.
Each establishment has a JSON `settings` column (toggles A–G + identity) driven from a super-admin Settings card. Iron rule: no option is ever decided client-side — a toggle hidden in the UI is also refused by the server. Cash tips are opt-in and private (separate table, visible to employee + super-admin only, never a normal admin), with a dual admin/employee role and an "employee view" toggle.
Non-obvious launch-day production bug: `/setup` returned a false 409 on an empty database. Cause: the password hash ran at 210k PBKDF2 iterations while Cloudflare Workers caps `deriveBits` at 100k — the call threw, masked by a catch. Brought down to 100k. The trap: it passed every Node test locally and only broke in production on the edge. Lesson reused on the other Workers projects.
The non-trivial challenge
Three mechanisms could leak data between restaurants: (1) the slug in the URL (`/<slug>/…`) — solved by making it non-authoritative, the tenant always comes from the server session; (2) super-admin impersonation ("Enter" an establishment) — a post-deploy bug meant "Enter" didn't refresh auth before navigating, so the router bounced back to /superadmin; fixed with refresh-before-navigate; (3) every DB query — each repository filters on establishment_id from the TenantContext, never a client parameter. All verified by an adversarial audit of 8 parallel agents, each hunting for a leak path: none found.
Lesson learned
A well-designed single-client tool pivots into a multi-tenant SaaS without a rewrite — as long as you NEVER trust the client to decide the tenant. The slug is cosmetic, the tenant comes from the session, and every repository filters on establishment_id hard. Second lesson, reused everywhere: crypto that passes locally can break on the edge — Cloudflare Workers caps PBKDF2 at 100k iterations, and a throw masked by a catch becomes a phantom bug that only shows in prod. Test on the target, not just in Node.
Features
Big start/stop buttons, long-duration confirmation — replaces the slow, fiddly round Android time picker that was painful mid-service
Daily entry, front-of-house distribution pro-rata to hours, exact to the cent (largest-remainder method), week/month recap
Formal draft → validated → paid → locked cycle, week/month view, CSV export for payroll
Employee records, email invitation, GDPR anonymization of former employees (right to be forgotten)
Global super-admin → establishments → admins (email invite), per-establishment slug non-authoritative server-side, establishment_id column + session TenantContext
Shared tablet account with 2 PINs: Server (view tips) and Manager (edit), no amounts shown in read-only consultation
Opt-in, visible only to the employee and super-admin — never to a normal admin. Dual admin/employee role with an "employee view" toggle
Journal of every action with the identity (email/name) of the account that made the change, employee-side error reporting