Aller au contenu

Architecture & hébergement

Stack validée

Couche Choix Remarque
Front React (PWA responsive) même code tablette + mobile
Back Node.js pas encore de code serveur écrit — le front parle directement à Supabase pour l'instant
Base de données Postgres via Supabase — remplace le SQLite envisagé au départ
Hébergement données/API Supabase multi-tenant partagé — un seul projet sert plusieurs établissements (voir ci-dessous, décision révisée le 2026-08-30)
Hébergement du front Cloudflare Pages site statique — voir ci-dessous pourquoi c'est séparé de Supabase

Pourquoi Supabase

Supabase fournit Postgres + Auth + Realtime managés. Le Realtime est particulièrement adapté au besoin « tout se passe à l'écran, pas de push » : dès qu'une ligne change dans alarmes, les appareils connectés reçoivent la mise à jour en direct, sans WebSocket ni serveur de push à coder soi-même.

Ce choix implique d'abandonner SQLite (envisagé initialement) au profit de Postgres — cohérent avec le besoin de suivi à distance et de connexion web permanente (contrairement à un hébergement local sur site, qui avait été envisagé un temps pour tenir en cas de panne réseau, écarté depuis que le mode dégradé accepté est un retour au papier).

Multi-tenant : gérer beaucoup de clients au moindre coût

Décision révisée le 2026-08-30

Le choix initial ("un projet Supabase par client, pas de multi-tenant") tenait pour quelques clients, mais ne passe pas à l'échelle économiquement. Décision remplacée par un modèle multi-tenant partagé, pensé pour un scénario du type "100 établissements clients".

Le calcul qui a motivé le changement

Approche Coût mensuel à 100 établissements
1 projet Supabase Pro par client 100 × 25 $ = 2 500 $/mois
1 seul projet Supabase partagé, isolation par colonne ~25-100 $/mois au total

Le stockage/calcul de cette app est minuscule par établissement (quelques Mo de données) — un seul projet Pro (8 Go inclus) tient large pour des dizaines, voire des centaines de clients.

Comment ça marche

Une table etablissements (le "tenant") et une colonne id_etablissement ajoutée sur chaque table métier (postes, personnel, devices, pairing_tokens, alarmes_types, schedules, alarmes, actions) — voir modèle de données. Chaque établissement ne voit que ses propres lignes.

Front (Cloudflare) : un seul déploiement du même code sert tous les clients (au lieu de 100 sites séparés — on aurait d'ailleurs buté sur la limite de 100 projets/compte du plan Free Cloudflare Pages, voir plus bas). Le client concerné se détermine au runtime (sous-domaine, ou identifiant à la connexion — pas encore implémenté côté front, voir points ouverts).

Ce qui n'est PAS encore fait — important avant un deuxième client réel

Isolation actuellement applicative, pas garantie par la base

La colonne id_etablissement existe et toutes les données de test y sont rattachées (établissement pilote fixe 00000000-0000-0000-0000-000000000001), mais Row Level Security (RLS) n'est pas activée — l'isolation entre clients repose entièrement sur le fait que chaque requête applicative filtre correctement par id_etablissement. Une erreur de code pourrait faire fuiter les données d'un client vers un autre. Activer RLS sans authentification bloquerait tout accès (le kiosque utilise actuellement la clé anon sans compte) : il faut d'abord brancher Supabase Auth (kiosque/téléphone authentifié, id_etablissement porté par le JWT), puis activer RLS avec des policies using (id_etablissement = ...). Ne pas onboarder de deuxième client réel avant d'avoir fait ça.

Compromis du multi-tenant partagé

  • Moins cloisonné qu'un projet par client — c'est le modèle standard de tout SaaS, le risque se gère (RLS + tests), mais le "blast radius" d'un bug est plus grand.
  • Voisin bruyant — un établissement très actif pourrait, en théorie, consommer plus de ressources et affecter les autres sur un même projet partagé.
  • En échange : une seule chose à maintenir (un dashboard, un déploiement, une mise à jour de schéma) au lieu de N — un vrai gain opérationnel en plus du gain de coût.

Coûts Supabase (vérifiés le 2026-08-29)

C'est quoi l'egress ?

Les données qui sortent du serveur vers l'app (par opposition à ingress, ce qui rentre) — c'est ce que la plupart des hébergeurs cloud facturent, car faire transiter des données coûte de la bande passante au fournisseur. Concrètement pour nous : chaque fois qu'une tablette/téléphone récupère des données depuis Supabase (liste des tâches, mise à jour Realtime, photo téléchargée depuis Storage), ça compte dans l'egress. Écrire vers Supabase (marquer une tâche "faite") ne compte généralement pas.

Plan Prix DB incluse Egress inclus Projets actifs Pause auto Backups
Free 0 $/mois 500 Mo 5 Go 2 max après 7 jours d'inactivité non
Pro à partir de 25 $/mois/projet (crédit compute 10 $/mois inclus) 8 Go (puis 0,125 $/Go) 250 Go (puis 0,09 $/Go) illimités jamais oui

Source : supabase.com/pricing

Recommandation : Free pour le prototype/démo. Passer en Pro (25 $/mois) dès qu'un client réel utilise l'app — deux raisons concrètes :

  • pas de backups sur Free, risqué pour un outil qui trace l'exécution réelle des tâches ;
  • la pause automatique après 7 jours d'inactivité est un vrai risque horeca (fermeture annuelle, travaux) : l'app serait suspendue au retour du client.

Notifications

Pas de Web Push pour l'instant (jugé trop complexe à ce stade). Les alertes s'affichent uniquement dans l'app, tant qu'elle est ouverte à l'écran — voir les limites de cette approche dans le besoin fonctionnel.

Projet Supabase actuel

Premier projet créé pour le développement/prototype (2026-08-29) — c'est désormais le projet partagé multi-tenant, pas un projet dédié à un client :

Référence projet niltnatkkiomejtexocy
Région eu-central-1
Plan Free (proto — passer en Pro avant tout usage par un client réel, voir ci-dessus)
Dashboard https://supabase.com/dashboard/project/niltnatkkiomejtexocy
Établissement pilote (dev) 00000000-0000-0000-0000-000000000001 — toutes les données de test actuelles

Voir Développement & connexion pour se connecter à ce projet et appliquer le schéma.

Hébergement du front — Cloudflare Pages

Supabase n'héberge pas de frontend. Ce n'est même pas sur leur feuille de route malgré des demandes répétées de la communauté — c'est une plateforme backend (base de données, auth, storage, Realtime, fonctions serveur), pas un hébergeur de site statique. Il faut donc un hébergeur séparé pour le code React : c'est l'architecture "Jamstack" standard (front statique + backend-as-a-service), pas un pis-aller.

On utilise Cloudflare Pages (déjà utilisé pour la doc d'autres projets) :

  • Gratuit et sans limite de bande passante ni de requêtes pour un site statique pur (notre cas : pas de "Pages Functions" côté Cloudflare, le front parle directement à Supabase depuis le navigateur). Limites réelles du plan Free : 100 projets/compte, 20 000 fichiers par site, 100 domaines personnalisés — aucun risque réaliste pour ce projet.
  • Projet front : alarmeshttps://alarmes.pages.dev
  • Intégration Git continue : chaque push sur main déclenche automatiquement build + déploiement — voir développement pour la config (build command, variables d'environnement).

Cette doc aussi : un second projet Cloudflare Pages, alarmes-doc, publie ce site MkDocs à partir du même dépôt → https://alarmes-doc.pages.dev, republié à chaque push. Deux projets Pages distincts sur un seul dépôt (monorepo), chacun avec son propre root_dir/build.

Doc publique, pas de restriction d'accès

alarmes-doc.pages.dev n'a aucune protection (pas de Zero Trust Access configuré) — n'importe qui avec le lien peut la lire. Rien de secret dedans (pas de token, pas de mot de passe), mais ça reste de l'info interne (architecture, référence du projet Supabase...). À restreindre plus tard si besoin (skill tools-doc-mails, déjà utilisée sur d'autres projets).

Sources : Frontend Hosting on Supabase Cloud (discussion officielle), Cloudflare Pages — Limits

Pourquoi pas tout sur Cloudflare (et abandonner Supabase) ?

Cloudflare a sa propre base de données (D1), mais ce n'est pas un remplaçant de Supabase pour ce projet :

  • D1 = SQLite, pas Postgres — on avait justement décidé d'abandonner SQLite pour Postgres (voir ci-dessus). Y aller reviendrait sur ce choix.
  • Pas de temps réel intégré — rien d'équivalent au Realtime de Supabase (écoute des changements de table, branché sur l'écran kiosque). Il faudrait le reconstruire à la main (Durable Objects/WebSockets).
  • Pas d'API générée automatiquement — Supabase expose une API REST directement à partir du schéma (PostgREST), ce qui permet au front de parler à la base sans code serveur. Avec D1 il faudrait écrire cette couche API soi-même dans un Worker.

Un seul fournisseur est séduisant sur le papier, mais impliquerait de ré-écrire ce qui fonctionne déjà pour arriver au même résultat, en moins bien équipé. Le découpage Cloudflare (front) + Supabase (données) est le partage de responsabilités standard, pas une limitation de notre setup.

Sources : Cloudflare D1 vs Neon vs Supabase Postgres 2026

Stockage d'images

Besoin identifié dès le schéma d'origine (AlarmesTypes.picture) et repris dans le handoff design (photo de preuve, pas encore implémentée — voir modèle de données).

Choix retenu : Supabase Storage plutôt que Cloudflare R2 — même fournisseur que la base de données, même client (supabase-js), pas de logique d'accès séparée à câbler. Suffisant pour notre échelle (photos de tâches horeca, pas de gros volume) :

Plan Quota inclus Taille max/fichier
Free 1 Go 50 Mo
Pro 100 Go (puis 0,0213 $/Go) 500 Go

Cloudflare R2 reste l'alternative si le volume/coût d'egress devenait un jour un vrai sujet (son argument principal : zéro frais de sortie) — mais ajoute un deuxième système de stockage à gérer séparément de la base, pour un gain qui ne se justifie qu'à grande échelle.

Compression côté client avant upload (pas encore implémentée) : les photos de téléphone sont souvent inutilement lourdes (plusieurs Mo en haute définition) pour une simple preuve visuelle. Prévu : redimensionner et recompresser dans le navigateur avant l'envoi (lib browser-image-compression, cible ~1600px / 500 Ko), ce qui réduit typiquement une photo de 3-8 Mo à 100-400 Ko. Détail dans développement.

Sources : Supabase Pricing