StaffClock
Live · clock.mindvisionstudio.comApp 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.
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
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.
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.
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é ».
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
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
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
Saisie quotidienne, répartition salle au prorata des heures, somme exacte au centime (méthode du plus grand reste), récap semaine/mois
Cycle formel draft → validé → payé → verrouillé, vue semaine/mois, export CSV pour la paie
Fiches employés, invitation par email, anonymisation RGPD des ex-salariés (droit à l'oubli)
Super-admin global → établissements → admins (invitation email), slug par établissement non-autoritaire côté serveur, colonne establishment_id + TenantContext de session
Compte partagé tablette avec 2 PIN : Serveur (voir les pourboires) et Manager (modifier), aucun montant en lecture seule
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é »
Journal de toutes les actions avec l'identité (email/nom) du compte qui a fait la modif, signalement d'erreurs côté salarié