Der Core-Server
openplate-core synchronisiert dein Tagebuch zwischen deinen Geräten. Es speichert eine E-Mail-Adresse und eine verschlüsselte Kopie deines Tagebuchs. Zudem verwahrt es deinen Wiederherstellungscode, geschützt durch ein eigenes Geheimnis, damit ein vergessenes Passwort dein Tagebuch wiederherstellt. Derselbe Code ermöglicht es der Person, die eine Instanz betreibt, ein Tagebuch darauf zu öffnen.
Was es lesen kann und was nicht
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.
Was er tatsächlich weiß
Offenheit bei den Metadaten, da „Ende-zu-Ende-verschlüsselt“ oft als „der Server weiß gar nichts“ missverstanden wird:
- Blob-Größe, und somit ein Näherungswert dafür, wie viele Daten das Konto umfasst. Die Komprimierung macht dieses Signal unschärfer als zuvor, verbirgt es aber nicht.
- Schreibhäufigkeit und Zeitpunkte: wann ein Gerät synchronisiert und wie oft.
- Versionsnummern:
blobVersion,envelopeVersionund die Anzahl der aufbewahrten Versionen. - KDF-Parameter und Salt für den Passphrasen-Datensatz. Das sind keine Geheimnisse; sie dienen dazu, an ein neues Gerät vor der Anmeldung ausgeliefert zu werden.
- Ob ein Konto die Einrichtung abgeschlossen hat (Schlüsseldatensätze vorhanden) und ob es jemals synchronisiert hat (Blob vorhanden).
- Das Konto selbst: eine E-Mail-Adresse, ein optionaler Anzeigename, eine Rolle, ein tägliches KI-Kontingent, ein Sperrzeitpunkt, ein Authentifizierungs-Prüfwert (ein mit Schlüssel versehener Hash eines mit Schlüssel versehenen Hashs der Passphrase, siehe §5.8), ein zweiter Prüfwert desselben Aufbaus über dem Wiederherstellungsnachweis und die KDF-Parameter des Kontos. Die Adresse bezeichnet eine reale Person, eine Kategorie personenbezogener Daten, die in 0.5.0 entfernt und in 0.6.0 bewusst wieder eingeführt wurde (ADR-0005): Personen einer Organisation werden über die Adresse identifiziert, an die ihre Einladung ging, da dies die Kennung ist, die sie auch in einem Monat noch kennen.
- Der WIEDERHERSTELLUNGSCODE des Kontos, versiegelt (
accounts.recovery_code_escrow, §3.1). Das ist der Eintrag in dieser Liste, an dem man innehalten sollte. Er ist mit AES-256-GCM unter einem Unterschlüssel vonSERVER_SECRETverschlüsselt, sodass ein reiner Datenbankabzug ihn nicht öffnet, die Betreiberinstanz einer gemanagten Instanz verfügt jedoch über beides. Der Betreiber einer verwalteten Instanz kann jedes Konto darauf öffnen. Nicht über einen Endpunkt und nicht über irgendeinen Codepfad in diesem Dienst, sondern durch direktes Auslesen dieser Spalte mit dem vorhandenen Secret und dem Ausführen der HKDF des Clients. Eine selbst gehostete Instanz ist ihre eigene Betreiberinstanz, dort gilt das frühere Versprechen also weiterhin. Die Entscheidung, einer gehosteten Instanz zu vertrauen, ist daher eine Entscheidung über deren Betreiber. - Ausstehende Einladungen: jeweils eine Adresse, ein optionaler Name, eine Rolle und ein Kontingent von jemandem, der noch KEIN Konto hat und keine Einwilligung erteilt hat. Das Erzeugen ist eine Operator-Aktion, und
DELETE /v1/admin/invites/:idzieht die Zeile zurück. Eine Zeile, die das Anfrageportal aus §5.8.3 erzeugt hat, ist als solche markiert, damit ein Operator sie zählen kann. Eine abgeschlossene Einladung verliert ihre Adresse und ihren Namen innerhalb einer Stunde: eine widerrufene oder abgelaufene Zeile auf jeder Instanz, eine eingelöste Zeile auf einer Instanz mitTRIAL_ADDRESS_PEPPER, die nur den unten stehenden Hash mit Schlüssel behält. Ohne den Pepper behält eine eingelöste Zeile ihre Adresse, weil die Regel zur erneuten Einladung von Mitgliedern aus §5.21 sie liest. - Die Scan-Testphase, auf einer Instanz, die eine betreibt: die gewährten und verbrauchten Gratis-Scans, zwei Ganzzahlen in der Kontozeile. Nur für ein Konto mit Scan-Testphase, eine Zeile pro KI-Aktion: eine vom Client gewählte, opake ID, eine Zeit, eine Anfrageanzahl und ob eine Antwort zugestellt wurde, 24 Stunden aufbewahrt und dann gelöscht, und nie protokolliert. Jede Einladungszeile trägt einen Einweg-Hash des Postfachs mit Schlüssel (HMAC-SHA256 unter
TRIAL_ADDRESS_PEPPER, ein Geheimnis, das nur der Betreiber besitzt, über den Testphasenschlüssel aus §5.8.3), niemals eine zweite Kopie der Adresse. - Nachdem ein Konto gelöscht wurde, auf einer Instanz, die eine Scan-Testphase betreibt: Adresse und Name werden aus jeder Einladungszeile zu diesem Postfach entfernt, und nur wenn das Konto eine Testphase nutzte, ein einzelner Hash des Postfachs mit Schlüssel wird behalten, und nichts anderes: kein Name, keine ID und ein Datum, der Zeitpunkt der Löschung, welcher die Zeile enden lässt. Dadurch wird verhindert, dass dasselbe Postfach eine zweite Testphase erhält. Ohne das Betreiber-Geheimnis kann der Hash weder umgekehrt noch mit einer Adressliste abgeglichen werden. Ein Bereinigungslauf löscht ihn
TRIAL_HASH_RETENTION_DAYS(standardmäßig 365) nach diesem Zeitpunkt, auf der Rechtsgrundlage von Art. 6 Abs. 1 lit. f DSGVO (ein berechtigtes Interesse an der Verhinderung des Missbrauchs kostenloser Scans, nicht juristisch geprüft, ADR-0010). Eine Instanz, die keine Scan-Testphase gewährt, speichert keinen Hash. Auf einer Instanz ohne Scan-Testphase behalten Einladungszeilen ihre Adresse nach einer Löschung für die Mitglieder-Wiedereinladungsregel aus §5.21. - KI-Nutzung: eine Ganzzahl pro Konto und UTC-Tag, wird 90 Tage lang aufbewahrt und dann gelöscht (§5.20). Ein Zähler, nie ein Protokoll: kein Prompt, keine Antwort, kein Modell, kein Zeitstempel über den Tag hinaus. Betreiber können die Zähler eines Kontos als tagesweises Band (
GET /v1/admin/accounts/:id/activity) abrufen, was Metadaten darüber darstellt, wann eine Person eine Gesundheits-App genutzt hat, und genau deshalb begrenzt ist. - Der Community-Puls, für Konten, die ihn aktiviert haben (§5.23, ADR-0007): instanzweite Tagessummen von Mahlzeiten, Fotos, Kalorien und Gramm Protein, eine Zeile pro beitragendem Konto und Tag sowie eine kurzlebige Präsenzzeile, die besagt, dass ein Konto gerade fastet. Die Summen lassen sich niemandem zuordnen; die Beitragszeile und die Präsenzzeile schon, und sie besagen lediglich „dieses Konto hat heute beigetragen“ und „dieses Konto fastet“. Tagessummen und Beitragszeilen werden 30 Tage lang aufbewahrt, die Präsenz läuft 30 Minuten nach dem letzten Heartbeat ab, und die Routen protokollieren keine Konto-ID. Wer ihn nie aktiviert hat, sendet nichts und taucht darin nirgends auf.
- Ein Push-Abonnement, für ein Gerät, dessen Besitzer Benachrichtigungen aktiviert hat (§5.24, ADR-0008): der Push-Dienst-Endpunkt, die beiden Schlüssel, mit denen er verschlüsselt, ein gekürzter User-Agent-String, eine IANA-Zeitzone, ein Gebietsschema, die Minute des lokalen Tages, zu der ein Nachholen fällig ist, der lokale Tag, an dem zuletzt eine Benachrichtigung rausging, der lokale Tag, an dem das Gerät zuletzt gesehen wurde, der Zeitpunkt, zu dem es geweckt werden wollte, und ein Zähler für das, was heute gesendet wurde. Zusammen verraten diese Angaben grob, wann diese Person wach ist, grob, wo auf der Welt sie sich befindet, und über
wake_at, wann ein Fasten von ihr endet. Letzteres deckt sich mit der Präsenzzeile des Pulses, was besagt, dass dasselbe Fasten läuft; ADR-0008 benennt diese Korrelation, statt sie erst entdecken zu lassen. Was NICHT gespeichert wird, ist auch nur ein Wort des Texts irgendeiner Benachrichtigung: Jeder Push übermittelt lediglich eine Art. Die Zeile verschwindet, wenn sich das Gerät abmeldet, wenn der Push-Dienst es verwehrt, oder zusammen mit dem Konto. - Eine Gesundheitsdaten-Einwilligung, auf einer Instanz, die danach fragt (§5.15.1): die Version des Wortlauts, dem die Person zugestimmt hat, und der Zeitpunkt, an dem dieser Dienst dies erfasst hat, zwei Spalten in der Kontozeile. Es besagt, dass die Person eine Gesundheits-App nutzt und eingewilligt hat, dass der Operator diese Daten verarbeitet, was der Operator nachweisen können muss. Für einen Operator ist es sichtbar (§5.20), keine Route löscht es, und bei einer Löschung verschwindet es zusammen mit der Kontozeile.
- Wann eine Person zuletzt aktiv war:
accounts.last_seen_at, geschrieben durch ein Login und durch eine weitergeleitete Vervollständigung, und bewusst nicht durch eine Token-Aktualisierung oder ein Sync-Polling, sodass es bedeutet, dass jemand aktiv war, und nicht bloß, dass ein Client lief. Es ist für Betreiber einsehbar (§5.20) und wird beim Löschen der Kontozeile mit entfernt. - Gesetzliche Erklärungen (
POST /v1/legal/declarations, eine Kündigung oder ein Widerruf, auf jeder Instanz): der Name, die Adresse, das Vertragszeichen, der Grund und die Daten, die die Person eingegeben hat, der Eingangszeitpunkt und das zugehörige Konto, falls vorhanden. Der Eintrag wird Aufbewahrung bis zum Ende des dritten Kalenderjahres nach dem Eingangsjahr, nach der Zeit in Europe/Berlin gerechnet, und danach durch den stündlichen Sweep gelöscht: Ein am 21.09.2026 eingegangener Eintrag wird ab dem 01.01.2030 um 00:00 Uhr in Berlin gelöscht. Das Löschen des Kontos löscht diesen Eintrag nicht vorzeitig; die Zeile verliert ihre Kontokennung und bleibt erhalten, da sie der Nachweis darüber ist, was die Person erklärt hat. Bestätigungsmails sind auf drei pro normalisierter Adresse, aufLEGAL_DECLARATION_RECEIPTS_PER_NETWORK_PER_DAY(standardmäßig 10) pro Absendernetzwerk und aufLEGAL_DECLARATION_RECEIPTS_PER_DAY(standardmäßig 200) pro Instanz begrenzt. Jedes Limit gilt für beliebige gleitende 24 Stunden. Die Gesamtzahlen werden aus diesen Zeilen ermittelt, mit Ausnahme des Netzwerkzählers, der im Arbeitsspeicher verbleibt. Eine Erklärung über einem Limit wird dennoch gespeichert, weitergeleitet und an den Betreiber gesendet, und der202ist derselbe. Die Bestätigung wiederholt niemals den Namen, das Vertragszeichen oder den Grund; nur die Kopie für den Betreiber enthält diese Angaben. - Sitzungsmetadaten: wie viele aktive Sitzungen existieren, wann jede einzelne erstellt wurde und wann Tokens zuletzt rotiert oder widerrufen wurden. Token-Werte selbst werden nur als Hashwerte gespeichert.
- Der Studiengraph, auf einem Server mit gesetztem
SYNC_RESEARCH(§5.18): welches Konto zu welcher Studie beiträgt, wann, wie oft und wie umfangreich jeder Beitrag ist. Eine Kante hier bedeutet "die Gesundheitsdaten dieser Person befinden sich in Studie Y", was gesundheitsnahe personenbezogene Daten derselben Kategorie wie die unten genannte Betreuungskante darstellt. Dies ist unvermeidbar, und der Widerruf belegt das: Das Löschen der Zeile eines Beitragenden erfordert deren Lokalisierung, eine Kontolöschung muss kaskadierend darüberlaufen, und sowohl das Compare-and-Swap als auch die Missbrauchskontrolle greifen auf das Konto zu. Ein Verfahren, das den Server blind dafür machen würde, würde mindestens eine dieser Funktionen stören, und eine Verkehrsanalyse würde die Verschleierung ohnehin wieder aufheben; deshalb wird dies offengelegt, statt halbe Ausweichwege zu suchen. Forschende erhalten diese Zuordnung niemals (§5.18 enthält keine Konto-ID), ein Widerruf löscht die Kante physisch und hinterlässt nur ein Pseudonym, und eine Installation ohne das gesetzte Flag besitzt gar keine Tabelle für einen Studiengraphen. - Der Freigabegraph, auf einem Deployment mit gesetztem
SYNC_SHARING(§5.16): welches Konto welchem anderen Konto Lesezugriff gewährt hat, wann die Freigabe erfolgte und wann die berechtigte Person darauf zugreift. Das bildet einen Beziehungsgraphen und stellt eine echte Erweiterung dessen dar, was dieser Dienst weiß. In dem Szenario, für das die Funktion gebaut wurde (eine behandelte Person und deren Ernährungsberatung), ist eine Kante in diesem Graphen selbst ein gesundheitsnahes personenbezogenes Datum, da sie aussagt, dass sich jemand in Behandlung befindet. Es ist das absolute Minimum, um den Lesezugriff zu autorisieren; beide Seiten stimmen zu, da die freigebende Seite die Zeile anlegt und die berechtigte Seite sie auf ihrer Seite löschen kann; und die Kante wird beim Widerruf endgültig gelöscht und kaskadierend entfernt, sobald eines der Konten gelöscht wird. Ein Deployment, dasSYNC_SHARINGnicht setzt, speichert keinen solchen Graphen und besitzt dafür keine Tabelle.
- Gemeldete Schätzungen, auf einem Deployment mit gesetztem
SYNC_FEEDBACK(§5.25, ADR-0006): die Zahlen jedes Eintrags, den eine Person gemeldet hat, das Tellerfoto, sofern das Gerät noch eines hatte, die Konto-ID, der Zustimmungsdatensatz und die Ankunftszeit, alles im Klartext. Sie werden fürinstance.feedback.retentionDaysaufbewahrt, dann von einem Sweep gelöscht, und sie verschwinden mit dem Konto. Betreiber können sie über §5.20 lesen, und jeder Lesezugriff auf ein Foto wird protokolliert. Ein Deployment ohne das Flag speichert weder Meldungen noch Fotos.
Aus den obigen Metadaten nicht erkennbar: was gegessen wurde, wann, wie viel oder sonstiges innerhalb der Payload. Zwei Einträge oben verraten es doch. Der versiegelte Wiederherstellungscode öffnet das gesamte Tagebuch für jeden, der auch SERVER_SECRET besitzt, und eine gemeldete Schätzung zeigt den einzelnen Eintrag, den sie enthält.
Verwaltete Instanzen
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.
Doku: Protokoll
Releases: Versionshinweise