L'application
Architecture
Les trois programmes et la base de données alimentaires, ce que chacun contient, et la façon dont ils s'articulent
Cette page est traduite automatiquement à partir de la documentation en anglais.
Trois programmes, un produit et deux modules facultatifs, plus un service externe qui répond aux noms d'aliments. Cette page détaille ce que chacun stocke, et qui se trouve sur le trajet de tes données.
Le schéma ci-dessous résume tout le système en cinq flèches. Ton appareil conserve le journal et la photo d'assiette. Le journal part chiffré vers openplate-core. La photo part vers l'API d'IA que tu as configurée. Le serveur applicatif fournit la page web, puis transmet les noms des aliments recherchés à une base de données d'aliments. Il ne se trouve ni sur le trajet du journal, ni sur celui des photos.
Source du schéma
flowchart LR
app["openplate app server"] -->|"the page"| device["Your device"]
device -->|"diary, encrypted"| sync["openplate-core"]
device -->|"photo"| ai["Your AI endpoint"]
device -->|"food names"| app
app -->|"food names"| fooddb["LowCarbCheck food database"]Trois options s'offrent pour « votre endpoint IA » : un fournisseur cloud chez qui tu détiens une clé, une machine openplate-inference sur ton propre matériel, ou, sur une instance gérée, le serveur central lui-même, qui transmet la photo et la décompte de ton quota. topologies.md illustre les quatre façons de faire tourner openplate, chacune avec un petit schéma.
Le client est le produit
Tout ce qu'un utilisateur possède (repas enregistrés, pesées, aliments personnels, objectifs, réglages de l'IA) est écrit dans l'IndexedDB du navigateur sur l'appareil où la saisie a eu lieu (app/lib/local-store/). Les données y sont stockées en clair, car c'est ton appareil, et elles n'en sortent jamais sauf sous deux formes que tu choisis : un export JSON que tu télécharges, ou un blob de synchronisation chiffré.
Le serveur applicatif est un simple conteneur sans état. Ni base de données, ni ORM, ni migrations, et aucun secret nécessaire pour démarrer. Il peut conserver exactement un secret optionnel, la clé de la base de données d'aliments de l'administrateur, décrite ci-dessous. Détruire le conteneur ne détruit rien. Ce n'est pas de la frugalité, c'est tout l'engagement du projet : voir ADR-0006.
La synchronisation gère l'identité, en dehors du circuit des photos et jamais dedans
openplate-core transfère un journal d'un appareil à l'autre. C'est le seul service d'openplate qui gère des comptes. Il s'agit d'un composant déployable distinct, avec sa propre image, sa base de données et son secret, et le navigateur communique directement avec lui. Le serveur de l'application ne relaie rien pour son compte et ne dessert aucune route de synchronisation.
Le journal est chiffré avant de quitter l'appareil. Le client sérialise la base locale, la compresse avec gzip, la chiffre en AES-256-GCM à l'aide d'une clé de données aléatoire, et envoie le résultat sous la forme d'un blob opaque unique. La clé de données est enveloppée sous une clé dérivée de ta phrase secrète : la phrase secrète est étirée avec Argon2id puis divisée par HKDF en branches indépendantes. Deux d'entre elles restent sur l'appareil pour déchiffrer ce qui t'appartient, et une troisième est envoyée comme identifiant de connexion. Ce sont des branches sœurs, sans lien de parenté direct, si bien que détenir l'identifiant ne révèle rien sur la clé.
L'opérateur détient une clé de récupération. Lors de l'inscription, l'application génère un code de récupération et y enveloppe la clé de données. Elle transmet le code à openplate-core, qui le scelle sous son propre secret. C'est ce mécanisme qui permet à une réinitialisation de mot de passe par courriel de restituer ton journal plutôt qu'un compte vide. Cela signifie également que la personne qui administre l'instance peut restaurer, et en principe lire, un journal stocké dessus. Sur une instance que tu héberges toi-même, cet administrateur, c'est toi. sync.md détaille l'arbitrage dans son ensemble.
Ce que le serveur voit en dehors du texte chiffré est décrit sans fard dans PROTOCOL.md §9 : une adresse courriel, la taille du blob, la fréquence et les heures d'écriture, les numéros de version, et les paramètres KDF.
La console d'étude facultative gère ses propres comptes distincts sur le même serveur. Synchronisation indique quand elle est active, et ADR-0008 explique pourquoi elle réside dans l'application.
L'inférence relève du calcul, et la photo lui est transmise directement
Une photo d'assiette est lue dans le navigateur et envoyée directement au point de terminaison compatible OpenAI que tu as configuré. Le serveur openplate n'intervient jamais dans cette requête. Elle n'est pas téléversée ici, pas écrite sur le disque, pas consignée dans les journaux. Seuls les chiffres résultants sont enregistrés, dans le stockage local de l'appareil ; la photo reste sur l'appareil qui l'a prise, exclue des exports JSON comme des charges utiles de synchronisation.
Ce point de terminaison est soit un fournisseur cloud que tu paies (la voie BYOK), soit ton propre conteneur openplate-inference. Dans le cas auto-hébergé, le modèle nomme les aliments dans l'assiette et estime les grammes, puis le macronutriments sont recherchés, pas inventés : les glucides, les protéines, les lipides et les kcal sont résolus par nom par rapport à la source d'aliments configurée, par défaut un extrait intégré de USDA FoodData Central (8 041 aliments génériques livrés dans l'image, aucun appel réseau, domaine public). Le modèle de langage ne génère jamais le moindre chiffre de macro.
Comme le navigateur effectue cet appel, le point de terminaison doit être une adresse qu'un navigateur peut joindre. Un nom d'hôte compose comme http://inference:8300/v1 ne fonctionnera pas, même si les deux conteneurs peuvent communiquer ainsi entre eux. Utilise l'adresse LAN de l'hôte, un nom tailnet, ou un nom d'hôte sur ton proxy inverse.
La base de données d'aliments fonctionne par recherche de nom, via le serveur applicatif
Un modèle hébergé ou géré dans le cloud renvoie sa propre estimation des macronutriments pour chaque aliment trouvé. L'application compare ensuite ces aliments à une base de données vérifiée. Le navigateur transmet uniquement les noms trouvés par le modèle au point d'accès /api/food-matches du serveur applicatif. Le serveur recherche chaque nom auprès de LowCarbCheck (FOOD_DB_API_URL). Un résultat concluant peut remplacer l'estimation du modèle sur l'écran de confirmation. La recherche sur l'écran Ajouter et les valeurs de référence sur l'écran Nutriments passent par cette même recherche côté serveur.
C'est l'unique flux où s'interpose le serveur applicatif, et il reste restreint par conception :
- Il ne transporte que des noms d'aliments, des noms de nutriments et la langue de l'interface. Jamais de photo, jamais ta clé d'accès à l'IA, jamais d'entrée du journal.
- LowCarbCheck voit l'adresse du serveur applicatif et la clé de l'instance, jamais ton adresse. Toutes les personnes sur une même instance partagent cette clé et son quota.
- Le serveur met les réponses en cache, et seules les recherches absentes du cache entrent dans le calcul de la limite de requêtes par adresse.
- Le système reste accessible par défaut. Si la base de données est indisponible, refuse la clé ou dépasse son quota, l'analyse se termine malgré tout avec les valeurs du modèle, et l'écran le signale.
FOOD_DB_API_URL=""le désactive, et dès lors aucun nom d'aliment ne quitte ton serveur.- Avec
FOOD_DB_BACKFILL=true, il transmet aussi des propositions : les noms d'un aliment qu'une personne a enregistrés à partir d'une réponse de l'IA, dans chaque langue de l'application, ainsi que ses macros pour 100 g pour un aliment sans correspondance. Jamais un nom saisi par la personne, jamais une photo ni une entrée du journal. Voir configuration.md.
Le traitement s'exécute sur le serveur plutôt que dans le navigateur. La clé ne se retrouve ainsi jamais dans la page, et le réglage défini par l'administrateur détermine si les noms sont transmis ou non. La clé correspond à FOOD_DB_API_KEY. Sans clé, l'instance utilise l'accès anonyme de LowCarbCheck ; configuration.md détaille ces niveaux d'accès.
Le serveur central gère le multi-tenant, et il se place devant la puissance de calcul sur une instance gérée
Une instance peut définir INSTANCE_MODE=managed (voir configuration.md). Cela déclare une chose : une organisation fait tourner cette instance, invite ses membres par e-mail et attribue à chacun un quota d'IA quotidien. openplate-core est ce qui porte cela, le compte qu'il détient déjà pour la synchronisation détient aussi le quota, il n'y a donc pas de seconde étape de connexion ni de second identifiant.
Pour le navigateur, rien ne change : un compte connecté disposant d'un quota effectue ses analyses via le proxy IA exposé par openplate-core, le même service auquel le client s'adresse déjà pour la synchronisation. Pour le système situé derrière, openplate-core agit comme un client : il pointe soit vers un fournisseur cloud, soit vers ton propre conteneur openplate-inference. L'inférence correspond à la couche de calcul, openplate-core forme la couche de gestion des accès sur une instance gérée, et les deux s'associent : le serveur central n'embarque aucun modèle et ne traite aucune analyse lui-même.
Il se trouve sur le chemin des photos, ce qui représente son coût réel, et la parade relève directement du code plutôt que d'un réglage : le type de champ du logger n'accepte que des types primitifs, de sorte qu'aucun corps de requête ne peut atterrir dans une ligne de log, et les messages d'erreur amont sont nettoyés avant d'être consignés ou renvoyés. Les membres d'une organisation mutualisent les dépenses, pas leurs données ; une photo d'assiette qui arrive sur le proxy est lue une seule fois et n'est pas conservée.
Le quota compte le nombre de requêtes, pas un montant financier. L'administrateur peut également plafonner l'ensemble de l'instance par jour (AI_INSTANCE_DAILY_LIMIT), et un plafond de dépenses sur la clé du fournisseur reste indispensable.
L'administration de l'instance s'effectue depuis /admin dans l'application : comptes et quotas associés, invitations, activité, estimations signalées, et choix des valeurs de référence affichées sur l'écran Nutriments.
Historique
D'août à septembre 2026, il s'agissait d'un service distinct, openplate-gateway : un petit proxy compatible avec OpenAI qui conservait une clé amont et attribuait à chaque membre un jeton opk_… avec son propre quota quotidien. La version M192 (septembre 2026) l'a fusionné dans openplate-core : un seul compte porte désormais à la fois le journal et le quota, il n'y a donc plus de second service, plus de second lien d'invitation et aucun second identifiant à distribuer.
Ce que openplate-core peut héberger d'autre
Chaque fonctionnalité ci-dessous est désactivée par défaut, et chacune modifie ce que le serveur conserve. Le fichier README de openplate-core détaille chacune d'elles.
- Partager un journal avec un praticien (
SYNC_SHARING=true). Le propriétaire enveloppe la clé de données une troisième fois, sous la clé publique du praticien, et le serveur stocke cette clé enveloppée. Le navigateur du praticien la déchiffre et consulte le journal sur/shared. Ce partage ne donne au serveur aucun élément nouveau à déchiffrer. - Contributions à la recherche (
SYNC_RESEARCH=true). Une personne participe à une étude à partir d'un lien et envoie des totaux journaliers sous pseudonyme. Les totaux sont scellés pour l'étude, mais le serveur apprend quel compte contribue à quelle étude. - Estimations signalées (
SYNC_FEEDBACK=true). « Signaler une mauvaise estimation » envoie la photo, les chiffres et un enregistrement de consentement au serveur, où un administrateur les examine sur/admin. Contrairement à un scan, cette photo est stockée. - Le pouls ne nécessite aucune configuration par l'administrateur, chaque personne l'active sous Paramètres, Partage. Il envoie des décomptes arrondis. Un repas compte une fois, avec ses calories arrondies à 50 et ses protéines à 5 g. Un scan compte une fois, et un jeûne en cours envoie un signal régulier. L'écran d'accueil montre le total de l'instance pour aujourd'hui. Le serveur conserve les totaux journaliers et un registre des personnes ayant contribué chaque jour, pendant 30 jours.
- Notifications push (
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT). Le serveur enregistre un abonnement par appareil, l'adresse de push du navigateur et ses clés, un fuseau horaire, une langue et les paramètres de rappel. Il envoie à chaque appareil au plus deux notifications par jour, et chacune ne transporte que son type, jamais de texte te concernant. Le service de push de ton navigateur la distribue. - Forfaits payants (
PLANS_UPSTREAM_URL,PLANS_UPSTREAM_SECRET). openplate-core relaie/v1/plans/*vers un service de forfaits géré par l'administrateur, avec l'identifiant du compte et l'adresse e-mail. L'application affiche Paramètres, Forfait seulement si le serveur indique que les forfaits sont activés.
Le BYOK représente l'option sans aucun serveur
Sans le moindre conteneur d'inférence, le navigateur appelle directement un fournisseur cloud à l'aide d'une clé que tu as saisie sur cet appareil. Cette clé reste stockée dans le stockage local de l'appareil, est exclue de l'export JSON et n'est jamais transmise au serveur openplate : il n'en existe aucune copie côté serveur, chiffrée ou non, et aucun relai de l'appel par proxy côté serveur.
La Content-Security-Policy de production garantit concrètement ce principe au lieu d'être un simple apparat : la liste d'autorisations connect-src découle du registre des fournisseurs, et c'est elle qui empêche un script injecté d'exfiltrer une clé présente dans la page. Voir configuration.md.
Qui détient quoi
| Composant | Ce qu'il stocke | Ce qu'il voit passer |
|---|---|---|
| Ton navigateur | L'intégralité du journal, en clair, dans IndexedDB. Ta clé d'IA. Les photos d'assiette en cache. | Absolument tout. C'est ton appareil. |
| Serveur d'application openplate | Pas de base de données, pas de comptes, pas de journal. Au plus un secret, la clé de l'administrateur pour la base de données alimentaire. | Les requêtes de pages, ainsi que les noms des aliments que tu cherches ou scannes, qu'il relaie vers la base de données alimentaire. Jamais une photo, jamais ta clé d'IA, jamais une entrée de journal, jamais un blob de synchronisation. |
| openplate-core (facultatif) | Une adresse e-mail, un vérificateur d'authentification, les paramètres de KDF, le journal sous forme de texte chiffré, et le code de récupération sous séquestre capable de le déchiffrer. Sur une instance gérée, également le quota journalier et le décompte d'utilisation de chaque compte. Si les fonctionnalités ci-dessus sont activées, également ce que chacune énumère. | Taille du blob, horodatage d'écriture, métadonnées de session. Sur une instance gérée, également la photo relayée au proxy d'IA, uniquement le temps de la relayer, lue une seule fois, non stockée. |
| openplate-inference (facultatif) | Rien par utilisateur : aucun compte, aucune session, aucun cookie. Les poids du modèle et un jeu de données alimentaires. | La photo que tu lui as envoyée, uniquement le temps que dure la requête. Avec la source alimentaire par défaut, il n'effectue aucun appel sortant, mis à part le téléchargement unique des poids. Avec FOOD_SOURCE=lcc ou off, il envoie des noms d'aliments à l'extérieur, jamais la photo. |
| Base de données d'aliments LowCarbCheck (activé sauf si désactivé) | Un décompte d'utilisation par clé, ou par adresse réseau pour un appelant qui n'en a pas. | Des noms d'aliments et une langue, en provenance du serveur de l'application, avec la clé de l'instance. Jamais une photo, et jamais ton identité. |
| Fournisseur d'IA dans le cloud (approche BYOK) | Ce que prévoient leurs règles de confidentialité. | La photo, et ta clé. Leurs conditions générales s'appliquent, pas les nôtres. |