← Retour aux projets

StaffClock

Live · clock.mindvisionstudio.com

App web interne de restaurant — pointage rapide, salaires estimés et répartition automatique des pourboires CB (salle/cuisine au prorata des heures, exacte au centime). Pivotée en SaaS multi-tenant : super-admin → établissements → admins, isolation stricte, compte Consultation tablette à PIN, vue salarié, audit complet. Cloudflare edge (Workers + D1 + R2 + Hono), React 19, RGPD France. Déployée en production.

FR EN
~25K Lignes TS
834 Tests
15 Tables DB
7 Migrations D1
12 Routes API
169 Commits

Pourquoi j'ai construit ça

Un restaurant comptait ses heures sur une appli Android lente — le sélecteur d'heure rond, impossible à utiliser vite en plein coup de feu. Les pourboires CB étaient répartis à la main, les salaires estimés au doigt mouillé. J'ai construit StaffClock pour ce besoin précis : pointage gros boutons, répartition salle automatique au centime près, salaires estimés, clôtures formelles. Une fois l'outil solide pour un resto, je l'ai pivoté en vrai SaaS multi-tenant — super-admin → établissements → admins par invitation — pour que n'importe quel établissement ait le sien, isolé du reste. Le tout sur Cloudflare edge (Workers + D1 + R2), ~5 $/mois, déployé en production sur clock.mindvisionstudio.com.

Décisions techniques structurantes

01
Multi-tenancy retrofit avec isolation stricte + audit adverse

Le pivot mono-resto → SaaS multi-tenant a ajouté une colonne establishment_id sur chaque table et un TenantContext porté par la session, jamais par l'URL. Le slug par établissement (`/<slug>/…`) est purement cosmétique : la connexion est globale et le serveur ne lui fait jamais confiance pour décider du tenant. Super-admin avec impersonation (« Entrer » dans un établissement → bandeau « Quitter »). L'isolation a été validée par un audit adverse de 8 agents cherchant une fuite cross-établissement : zéro fuite trouvée.

02
Répartition des pourboires exacte au centime

La répartition salle se fait au prorata des heures travaillées, mais la somme des parts doit retomber exactement sur le total saisi — pas d'arrondi qui crée ou détruit un centime. Implémenté via la méthode du plus grand reste : on calcule les parts entières en centimes, puis on distribue les centimes restants aux plus grands restes. Vues Jour / Semaine / Mois, et aucun montant visible en consultation lecture seule.

03
Feature-toggles par établissement, tous gated côté serveur

Chaque établissement a une colonne JSON `settings` (toggles A–G + identité) pilotée depuis une carte Réglages super-admin. Règle de fer : aucune option n'est jamais décidée côté client — un toggle masqué dans l'UI est aussi refusé par le serveur. Pourboires espèces en opt-in privé (table séparée, visibles salarié + super-admin uniquement, jamais admin normal), double rôle admin/salarié avec bascule « vision salarié ».

04
Crypto durcie sous le plafond CPU de Cloudflare Workers

Bug prod non-évident au lancement : `/setup` renvoyait un faux 409 sur base vide. Cause : le hash de mot de passe tournait à 210k itérations PBKDF2 alors que Cloudflare Workers plafonne `deriveBits` à 100k — l'appel throwait, masqué par un catch. Ramené à 100k. Le piège : ça passait tous les tests Node en local et ne cassait qu'en production sur l'edge. Leçon réutilisée sur les autres projets Workers.

Le challenge non-trivial

Garantir l'isolation entre établissements quand le super-admin peut impersonner et que l'URL porte un slug

Trois mécanismes pouvaient fuiter des données d'un resto à l'autre : (1) le slug dans l'URL (`/<slug>/…`) — résolu en le rendant non-autoritaire, le tenant vient toujours de la session serveur ; (2) l'impersonation super-admin (« Entrer » dans un établissement) — un bug post-deploy faisait que « Entrer » ne rafraîchissait pas l'auth avant de naviguer, le routeur renvoyait sur /superadmin ; corrigé en refresh-avant-navigate ; (3) chaque requête DB — chaque repository filtre sur establishment_id issu du TenantContext, jamais d'un paramètre client. Le tout vérifié par un audit adverse de 8 agents en parallèle, chacun cherchant un chemin de fuite : aucune fuite.

Leçon retenue

Un outil mono-client bien conçu se pivote en SaaS multi-tenant sans réécriture — à condition de ne JAMAIS faire confiance au client pour décider du tenant. Le slug est cosmétique, le tenant vient de la session, et chaque repository filtre sur establishment_id en dur. Deuxième leçon, réutilisée partout : la crypto qui passe en local peut casser sur l'edge — Cloudflare Workers plafonne PBKDF2 à 100k itérations, et un throw masqué par un catch se transforme en bug fantôme qui ne sort qu'en prod. Tester sur la cible, pas seulement en Node.

Fonctionnalités

⏱️
Pointage rapide

Gros boutons début/fin, confirmation des longues durées — remplace le sélecteur d'heure rond Android, lent et pénible en plein service

💶
Pourboires CB automatiques

Saisie quotidienne, répartition salle au prorata des heures, somme exacte au centime (méthode du plus grand reste), récap semaine/mois

🧾
Clôtures & export CSV

Cycle formel draft → validé → payé → verrouillé, vue semaine/mois, export CSV pour la paie

👥
Personnes & RGPD

Fiches employés, invitation par email, anonymisation RGPD des ex-salariés (droit à l'oubli)

🔐
Multi-tenant isolé

Super-admin global → établissements → admins (invitation email), slug par établissement non-autoritaire côté serveur, colonne establishment_id + TenantContext de session

📱
Tablette Consultation à PIN

Compte partagé tablette avec 2 PIN : Serveur (voir les pourboires) et Manager (modifier), aucun montant en lecture seule

🙈
Pourboires espèces privés

Opt-in, visibles uniquement par le salarié et le super-admin — jamais par un admin normal. Double rôle admin/salarié avec bascule « vision salarié »

🛡️
Audit complet

Journal de toutes les actions avec l'identité (email/nom) du compte qui a fait la modif, signalement d'erreurs côté salarié

Stack technique

Runtime
Cloudflare Workers (edge)nodejs_compat
Backend
Hono 4TypeScript strictZod 4RBAC sessions
Données
Cloudflare D1 (SQLite)Drizzle ORM15 tables7 migrations
Médias / Email
R2 (photos)Resend (invitations + démo)
Frontend
React 19React Router 7Tailwind CSS 4Vite 8
Tests / CI
Vitest 4 (834 tests)@testing-library/reactjsdom