Zum Inhalt springen
openplate

Die App

Architektur

Die drei Programme und die Lebensmitteldatenbank, was jedes davon enthält, und wie sie zusammenspielen

Diese Seite wurde maschinell aus der englischen Dokumentation übersetzt.

Drei Programme, ein Produkt und zwei optionale Anhänge, plus ein externer Dienst, der Lebensmittelnamen beantwortet. Diese Seite erklärt, was jedes davon speichert und welches davon im Pfad deiner Daten steht.

Die Zeichnung unten zeigt das gesamte System in fünf Pfeilen. Dein Gerät speichert das Tagebuch und das Tellerfoto. Das Tagebuch verlässt das Gerät verschlüsselt in Richtung openplate-core. Das Foto geht an den KI-Endpunkt, den du konfiguriert hast. Der App-Server liefert die Seite aus und leitet die Namen der nachgeschlagenen Lebensmittel an eine Lebensmitteldatenbank weiter. Er steht weder im Tagebuchpfad noch im Fotopfad.

Das Gerät speichert das Tagebuch und das Foto. Das Tagebuch verlässt das Gerät verschlüsselt in Richtung openplate-core, das Foto geht an den von dir konfigurierten KI-Endpunkt, und Lebensmittelnamen gehen über den App-Server an die Lebensmitteldatenbank.
Quellcode des Diagramms
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"]

Drei Dinge passen hinter „dein KI-Endpunkt“: ein Cloud-Anbieter, bei dem du einen Schlüssel hast, ein openplate-inference-Rechner auf eigener Hardware oder, bei einer gemanagten Instanz, der Core-Server selbst, der das Foto weiterleitet und von deinem Kontingent abzieht. topologies.md zeigt alle vier Möglichkeiten, openplate zu betreiben, jeweils als kleine Grafik.

Der Client ist das Produkt

Alles, was einem Nutzer gehört (Ernährungsprotokolle, Gewichte, eigene Lebensmittel, Ziele, die KI-Einstellungen), wird im IndexedDB des Browsers auf dem Gerät gespeichert, auf dem es eingegeben wurde (app/lib/local-store/). Es liegt dort im Klartext, weil es dein Gerät ist, und verlässt es nie, außer in zwei von dir gewählten Formen: als JSON-Export, den du herunterlädst, oder als verschlüsselter Sync-Blob.

Der App-Server ist ein einzelner zustandsloser Container. Keine Datenbank, kein ORM, keine Migrationen und kein Secret, das er zum Starten benötigt. Er kann genau ein optionales Secret enthalten, den Schlüssel des Betreibers für die Lebensmitteldatenbank, wie unten beschrieben. Das Löschen des Containers führt zu keinem Datenverlust. Das ist keine Sparsamkeit, es ist das gesamte Versprechen: siehe ADR-0006.

Synchronisation dient der Identität, liegt neben dem Fotopfad und niemals darin

openplate-core überträgt ein Tagebuch zwischen Geräten. Es ist der einzige Dienst in openplate, der überhaupt Konten führt. Es ist ein separates Bereitstellungsobjekt mit eigenem Image, eigener Datenbank sowie eigenem Secret, und der Browser kommuniziert direkt damit. Der App-Server leitet nichts stellvertretend weiter und bedient keine Sync-Route.

Das Tagebuch wird verschlüsselt, bevor es das Gerät verlässt. Der Client serialisiert den lokalen Speicher, komprimiert ihn mit gzip, verschlüsselt ihn mit AES-256-GCM unter einem zufälligen Datenschlüssel und lädt das Ergebnis als einen opaken Blob hoch. Der Datenschlüssel wird unter einem aus deiner Passphrase abgeleiteten Schlüssel verpackt: Die Passphrase wird mit Argon2id gestreckt und per HKDF in unabhängige Zweige aufgeteilt. Zwei davon verbleiben auf dem Gerät und entschlüsseln deine Daten, und ein dritter wird als Anmeldedaten übertragen. Sie sind Geschwister, nicht Elternteil und Kind, weshalb der Besitz der Anmeldedaten nichts über den Schlüssel verrät.

Der Betreiber hält einen Wiederherstellungsschlüssel. Bei der Registrierung erzeugt die App einen Wiederherstellungscode und verpackt den Datenschlüssel darunter. Sie sendet den Code an openplate-core, das ihn unter seinem eigenen Secret versiegelt. Dadurch stellt ein per E-Mail angefordertes Zurücksetzen des Passworts dein Tagebuch wieder her, statt ein leeres Konto zu liefern. Es bedeutet aber auch, dass der Betreiber einer Instanz ein darauf liegendes Tagebuch wiederherstellen und prinzipiell lesen kann. Auf einer Instanz, die du selbst hostest, bist du dieser Betreiber. sync.md legt den Kompromiss vollständig dar.

Was der Server neben dem Geheimtext sieht, ist in PROTOCOL.md §9 klar aufgeführt: eine E-Mail-Adresse, Blob-Größe, Schreibhäufigkeit und Zeitpunkte, Versionsnummern und KDF-Parameter.

Die optionale Studienkonsole führt eigene, separate Benutzerkonten auf demselben Server. Synchronisation beschreibt, wann sie aktiv ist, und ADR-0008 erklärt, warum sie Teil der App ist.

Inferenz ist Rechenleistung, und das Foto geht direkt dorthin

Ein Tellerfoto wird im Browser eingelesen und direkt an den OpenAI-kompatiblen Endpunkt geschickt, den du konfiguriert hast. Der openplate-Server ist an dieser Anfrage nie beteiligt. Es wird hier nicht hochgeladen, nicht auf die Festplatte geschrieben, nicht protokolliert. Nur die berechneten Zahlen werden gespeichert, im lokalen Speicher des Geräts; das Foto verbleibt auf dem Gerät, das es aufgenommen hat, ausgeschlossen von JSON-Exporten wie auch von Sync-Nutzdaten.

Dieser Endpunkt ist entweder ein Cloud-Anbieter, den du bezahlst (der BYOK-Pfad), oder dein eigener openplate-inference-Container. Im selbstgehosteten Fall benennt das Modell die Lebensmittel auf dem Teller und schätzt die Grammzahl, und dann werden Makronährstoffe nachgeschlagen, nicht erfunden: Kohlenhydrate, Protein, Fett und kcal werden anhand des Namens über die konfigurierte Lebensmittelquelle aufgelöst, standardmäßig ein mitgelieferter Auszug aus USDA FoodData Central (8.041 generische Lebensmittel im Image enthalten, kein Netzwerkaufruf, gemeinfrei). Das Sprachmodell erfindet niemals Makronährstoffwerte.

Da der Browser diesen Aufruf ausführt, muss der Endpunkt eine Adresse sein, die ein Browser erreichen kann. Ein Compose-Hostname wie http://inference:8300/v1 funktioniert nicht, auch wenn sich die beiden Container darüber erreichen können. Verwende die LAN-Adresse des Hosts, einen Tailnet-Namen oder einen Hostnamen auf deinem Reverse-Proxy.

Die Lebensmitteldatenbank ist ein Nachschlagen nach Namen über den App-Server

Ein Cloud- oder Managed-Modell liefert eine eigene Makronährstoffschätzung für jedes erkannte Lebensmittel zurück. Die App gleicht diese Lebensmittel anschließend mit einer kuratierten Datenbank ab. Der Browser sendet nur die vom Modell gefundenen Namen an /api/food-matches des App-Servers. Der Server schlägt jeden Namen bei LowCarbCheck nach (FOOD_DB_API_URL). Ein Treffer kann die Schätzung des Modells in der Bestätigungsansicht ersetzen. Die Suche in der Hinzufügen-Ansicht und die Referenzwerte in der Nährstoffe-Ansicht laufen über denselben serverseitigen Abruf.

Dies ist der einzige Pfad, in dem der App-Server liegt, und er ist konstruktionsbedingt schmal:

  • Er transportiert Lebensmittelnamen, Nährstoffbezeichnungen und die Sprache der Benutzeroberfläche. Niemals ein Foto, niemals deinen KI-Schlüssel, niemals einen Tagebucheintrag.
  • LowCarbCheck sieht die Adresse des App-Servers und den Schlüssel der Instanz, niemals deine Adresse. Alle Nutzer auf einer Instanz teilen sich diesen Schlüssel und sein Kontingent.
  • Der Server speichert Antworten zwischen, und nur ein Abruf ohne Cache-Treffer zählt gegen eine Ratenbegrenzung pro Adresse.
  • Er verhält sich im Fehlerfall durchlässig (fail open). Wenn die Datenbank unerreichbar ist, den Schlüssel ablehnt oder das Kontingent aufgebraucht ist, wird der Scan dennoch mit den Werten des Modells abgeschlossen, und der Bildschirm weist darauf hin.
  • FOOD_DB_API_URL="" schaltet es ab, und dann verlässt kein Lebensmittelname deinen Server.
  • Mit FOOD_DB_BACKFILL=true überträgt er auch Vorschläge: die Namen eines Lebensmittels, das jemand aus einer KI-Antwort gespeichert hat, in jeder App-Sprache, und bei einem Lebensmittel ohne Treffer dessen Makronährstoffe pro 100 g. Niemals einen Namen, den die Person selbst eingetippt hat, niemals ein Foto oder einen Tagebucheintrag. Siehe configuration.md.

Es läuft auf dem Server statt im Browser. Das hält den Schlüssel von der Seite fern und überlässt der Betreiberkonfiguration die Entscheidung, ob Namen überhaupt übertragen werden. Der Schlüssel ist FOOD_DB_API_KEY. Ohne Schlüssel nutzt die Instanz die anonyme Tarifstufe von LowCarbCheck; configuration.md listet die Stufen auf.

Der Core-Server verwaltet Mandanten und sitzt auf einer gemanagten Instanz vor der Rechenleistung

Eine Instanz kann INSTANCE_MODE=managed setzen (siehe configuration.md). Das legt genau eines fest: eine Organisation betreibt diese Instanz, lädt ihre Mitglieder per E-Mail ein und weist jedem ein tägliches KI-Kontingent zu. openplate-core übernimmt das, und das Konto, das es bereits für die Synchronisierung führt, enthält auch das Kontingent. Es gibt also keinen zweiten Verbindungsschritt und keine zweiten Zugangsdaten.

Für den Browser ändert sich nichts: Ein angemeldetes Konto mit Kontingent scannt über den KI-Proxy, den openplate-core bereitstellt, denselben Dienst, den der Client bereits für den Abgleich nutzt. Für das System dahinter ist openplate-core ein Client: Er verweist entweder auf einen Cloud-Anbieter oder deinen eigenen openplate-inference-Container. Inferenz bildet die Rechenschicht, openplate-core die Mandantenschicht auf einer verwalteten Instanz, und beide greifen ineinander: Der Core-Server hält kein Modell vor und beantwortet Scans nicht selbst.

Der Dienst liegt auf dem Pfad des Fotos, das ist der reale Preis dafür, und der Schutz ist eine Eigenschaft des Codes und keine Einstellung: Der Feldtyp des Loggers lässt nur primitive Datentypen zu, sodass kein Body in eine Logzeile gelangen kann, und Upstream-Fehlertexte werden bereinigt, bevor sie protokolliert oder zurückgegeben werden. Mitglieder einer Organisation teilen sich die Kosten, nicht die Daten; ein Tellerfoto, das den Proxy erreicht, wird einmal gelesen und nicht gespeichert.

Das Kontingent zählt Anfragen, kein Geld. Ein Betreiber kann die gesamte Instanz auch pro Tag deckeln (AI_INSTANCE_DAILY_LIMIT), und ein Ausgabenlimit für den Upstream-Schlüssel beim Anbieter ist weiterhin erforderlich.

Ein Administrator verwaltet die Instanz über /admin in der App: Personen und ihre Kontingente, Einladungen, Aktivität, gemeldete Schätzungen und welche Referenzwerte die Nährstoffe-Ansicht anzeigt.

Entwicklungsgeschichte

Von August bis September 2026 war dies ein eigenständiger Dienst namens openplate-gateway: ein kleiner OpenAI-kompatibler Proxy, der einen Upstream-Schlüssel verwaltete und jedem Mitglied ein opk_…-Token mit eigenem Tageskontingent ausstellte. M192 (September 2026) führte ihn mit openplate-core zusammen: Ein einziges Konto umfasst nun sowohl das Tagebuch als auch das Kontingent, sodass kein zweiter Dienst, kein zweiter Einladungslink und keine zweiten Zugangsdaten mehr vergeben werden müssen.

Was openplate-core sonst noch speichern kann

Jede der folgenden Funktionen ist standardmäßig deaktiviert, und jede ändert, was der Server speichert. openplate-cores README beschreibt jede einzelne.

  • Ein Tagebuch für einen Behandler freigeben (SYNC_SHARING=true). Der Eigentümer verpackt den Datenschlüssel ein drittes Mal unter dem öffentlichen Schlüssel des Behandlers, und der Server speichert diesen verpackten Schlüssel. Der Browser des Behandlers entpackt ihn und liest das Tagebuch unter /shared. Die Freigabe gibt dem Server nichts Neues zum Entschlüsseln.
  • Forschungsbeiträge (SYNC_RESEARCH=true). Eine Person nimmt über einen Link an einer Studie teil und sendet tägliche Summen unter einem Pseudonym. Die Summen sind für die Studie versiegelt, aber der Server erfährt, welches Konto zu welcher Studie beiträgt.
  • Gemeldete Schätzungen (SYNC_FEEDBACK=true). „Falsche Schätzung melden“ sendet das Foto, die Zahlen und einen Einwilligungseintrag an den Server, wo ein Administrator sie unter /admin prüft. Anders als bei einem Scan wird dieses Foto gespeichert.
  • Der Puls benötigt keine Betreiber-Einstellung: Jede Person aktiviert es unter Einstellungen, Teilen selbst. Es sendet gerundete Zahlen. Eine Mahlzeit zählt einmal, wobei ihre Kalorien auf 50 und ihr Protein auf 5 g gerundet werden. Ein Scan zählt einmal, und ein laufendes Fasten sendet ein Lebenszeichen. Der Startbildschirm zeigt die heutige Gesamtsumme der Instanz. Der Server speichert Tagessummen und für 30 Tage den Nachweis, wer an welchem Tag beigetragen hat.
  • Push-Benachrichtigungen (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT). Der Server speichert ein Abonnement pro Gerät: die Push-Adresse des Browsers und deren Schlüssel, eine Zeitzone, eine Sprache und die Erinnerungseinstellungen. Er sendet jedem Gerät höchstens zwei Benachrichtigungen am Tag, und jede enthält nur ihre Art, niemals Text über dich. Der Push-Dienst deines Browsers stellt sie zu.
  • Kostenpflichtige Tarife (PLANS_UPSTREAM_URL, PLANS_UPSTREAM_SECRET). openplate-core leitet /v1/plans/* zusammen mit der Konto-ID und der E-Mail-Adresse an einen vom Betreiber betriebenen Tarif-Dienst weiter. Die App zeigt Einstellungen, Tarif nur an, wenn der Server meldet, dass Tarife aktiv sind.

BYOK ist der serverlose Pfad

Ganz ohne Inferenz-Container ruft der Browser einen Cloud-Anbieter direkt mit einem Schlüssel auf, den du auf diesem Gerät eingegeben hast. Der Schlüssel liegt im lokalen Gerätespeicher, bleibt vom JSON-Export ausgeschlossen und wird nie an den openplate-Server gesendet: Es gibt keine serverseitige Kopie, verschlüsselt oder anderweitig, und keinen serverseitigen Proxy für den Aufruf.

Die Content-Security-Policy in der Produktionsumgebung ist Teil dieses Versprechens und keine reine Dekoration: Die connect-src-Freigabeliste leitet sich aus der Anbieter-Registry ab und verhindert, dass ein eingeschleustes Skript einen auf der Seite liegenden Schlüssel entwendet. Siehe configuration.md.

Wer welche Daten hält

KomponenteWas gespeichert wirdWas bei der Übertragung sichtbar ist
Dein BrowserDas gesamte Tagebuch, im Klartext, in IndexedDB. Dein KI-Schlüssel. Zwischengespeicherte Tellerfotos.Alles. Es ist dein Gerät.
openplate-App-ServerKeine Datenbank, keine Konten, kein Tagebuch. Höchstens ein Geheimnis: der Schlüssel des Betreibers für die Lebensmitteldatenbank.Seitenabrufe und die Namen der Lebensmittel, die du nachschlägst oder scannst, welche an die Lebensmitteldatenbank weitergeleitet werden. Niemals ein Foto, niemals dein KI-Schlüssel, niemals ein Tagebucheintrag, niemals ein Sync-Blob.
openplate-core (optional)Eine E-Mail-Adresse, ein Authentifizierungs-Verifier, KDF-Parameter, das Tagebuch als Geheimtext und der treuhänderisch verwahrte Wiederherstellungscode, der ihn entschlüsseln kann. Auf einer verwalteten Instanz außerdem das tägliche Kontingent und der Nutzungszähler jedes Kontos. Wenn die oben genannten Funktionen aktiviert sind, auch das, was jede von ihnen aufführt.Blob-Größe, Schreibzeitpunkte, Sitzungs-Metadaten. Auf einer verwalteten Instanz außerdem das an den KI-Proxy weitergeleitete Foto für die Dauer der Weiterleitung, einmal gelesen, nicht gespeichert.
openplate-inference (optional)Nichts pro Nutzer: keine Konten, keine Sitzungen, keine Cookies. Nur Modellgewichte und ein Lebensmitteldatensatz.Das Foto, das du gesendet hast, für die Dauer der Anfrage. Mit der Standard-Lebensmittelquelle erfolgt außer dem einmaligen Download der Gewichte kein ausgehender Aufruf. Mit FOOD_SOURCE=lcc oder off werden Lebensmittelnamen nach außen gesendet, niemals das Foto.
LowCarbCheck-Lebensmitteldatenbank (aktiv, sofern nicht deaktiviert)Ein Nutzungszähler pro Schlüssel oder pro Netzwerkadresse für Aufrufer ohne Schlüssel.Lebensmittelnamen und eine Sprache vom App-Server mit dem Schlüssel der Instanz. Niemals ein Foto und niemals deine Identität.
Cloud-KI-Anbieter (BYOK-Pfad)Was auch immer deren Richtlinie besagt.Das Foto und dein Schlüssel. Es gelten deren Bedingungen, nicht unsere.

Diese Seite auf GitHub bearbeiten