Zum Inhalt springen
openplate

Die App

Konfiguration

Der Schlüssel für die Lebensmittel-Datenbank, die Content-Security-Policy, Analytics, verwaltete Instanzen, eigene und von der Instanz bereitgestellte KI-Endpunkte

Diese Seite wurde maschinell aus der englischen Dokumentation übersetzt.

openplate startet ganz ohne Konfiguration. Es gibt keine Datenbank-URL, keinen Sitzungsschlüssel und keinen Verschlüsselungsschlüssel, da der Server keine Benutzerkonten führt und nichts speichert. Jede Variable dient nur der optionalen Feinabstimmung.

Die meisten Variablen werden an einer einzigen Stelle eingelesen, app/config/index.ts, und als typisiertes CONFIG-Objekt bereitgestellt:

typescript
import { CONFIG } from '#config';

const port = CONFIG.server.port;
const appUrl = CONFIG.app.url;

.env.example enthält die vollständige Liste samt eingebetteten Hinweisen. Kopiere die Datei nach .env, um Werte anzupassen.

Umgebungsvariablen

environment-variables.md listet jede Variable auf, die die App liest, mitsamt ihrem Standardwert. Es listet außerdem die Variablen für den Core-Server und den Inferenzdienst auf. Die folgenden Abschnitte erklären die größeren Funktionen ausführlich.

Eine Variable hat keinen eigenen Abschnitt. DEFAULT_UI_LANGUAGE legt die Sprache fest, die Besucher sehen, bevor sie eine auswählen: en, der Standardwert, de, fr, it, es oder tr. Die eigene Auswahl einer Person hat immer Vorrang. Die Einstellung übersetzt keinen Namen eines Lebensmittels, keine KI-Antwort und nichts, was eine Person eingegeben hat. Jeder andere Wert bricht den Startvorgang ab.

API-Schlüssel der Anbieter werden nie aus der Umgebung ausgelesen. Den Schlüssel gibt der Nutzer im Browser ein, er wird auf dem Gerät gespeichert und direkt vom Browser an den Anbieter gesendet, der Server besitzt keine Kopie. MISTRAL_API_KEY / OPENROUTER_API_KEY in .env.example existieren nur, damit Entwickler Prüfskripte gegen einen echten Anbieter ausführen können. Auf einer Produktivinstanz bewirken sie nichts.

Der Schlüssel für die Lebensmitteldatenbank

FOOD_DB_API_URL und FOOD_DB_API_KEY sind zwei verschiedene Entscheidungen, und es hilft, sie auseinanderzuhalten.

Die URL bestimmt, ob diese Instanz Lebensmittel überhaupt nachschlägt. Setze sie auf eine leere Zeichenkette, und kein Lebensmittelname verlässt jemals deinen Rechner.

Der Schlüssel bestimmt, wie viel du nachschlagen darfst. Es gibt drei Stufen:

Stufewas du tustwas du bekommst
anonymnichtsein kleines tägliches Kontingent, geteilt pro Netzwerkadresse
kostenloseine E-Mail-Adresse unter lowcarbcheck.org/developers angebenein großzügiges monatliches Kontingent
Partnernachfragenkeine monatliche Obergrenze

LowCarbCheck zählt in Credits, und eine Lebensmittelsuche kostet einen. Zum Zeitpunkt der Erstellung dieses Textes umfasst die anonyme Stufe 1.000 Credits pro Tag und der kostenlose Schlüssel 100.000 pro Monat; lowcarbcheck.org/developers enthält die aktuellen Zahlen. LowCarbCheck sieht die Adresse deines App-Servers, nicht die Adressen seiner Nutzer. Auf der anonymen Stufe teilen sich alle auf deiner Instanz ein tägliches Kontingent.

Eine Instanz ohne Schlüssel funktioniert weiterhin. Das ist die anonyme Stufe, und für eine Person, die openplate ausprobiert, reicht sie normalerweise aus. Ein Haushalt oder jede Nutzung, bei der täglich mehrere Teller gescannt werden, benötigt den kostenlosen Schlüssel.

FOOD_DB_DAILY_CALL_LIMIT begrenzt, wie viele LowCarbCheck-Aufrufe dieser Server an einem UTC-Tag ausführt. Der Standardwert 3.200 hält einen Monat innerhalb der 100.000 des kostenlosen Schlüssels. Ein Name, den jemand in den letzten fünf Minuten gesucht hat, wird aus dem Speicher beantwortet und kostet nichts. Nach Erreichen des Limits pausieren Lebensmittelsuchen bis Mitternacht UTC, die App weist darauf hin, und Scans werden weiterhin mit den eigenen Werten der KI abgeschlossen. Erhöhe den Wert, wenn dein Schlüssel mehr erlaubt. Der Zähler liegt im Speicher, daher startet ein Neustart ihn wieder von vorn.

Auf einer verwalteten Instanz erfordert eine Lebensmittelsuche ebenfalls ein angemeldetes Konto. Die App sendet die Sitzung des Kontos mit jeder Suche mit, und der App-Server fragt den Core-Server unter CORE_URL, ob die Sitzung aktiv ist, bevor irgendetwas LowCarbCheck erreicht. Der App-Server muss diese Adresse daher ebenfalls erreichen können. Wenn er das nicht kann, werden Suchen abgewiesen, bis er es kann, und Scans werden weiterhin mit den eigenen Werten der KI abgeschlossen. Eine offene Instanz beantwortet jede Suche wie bisher.

Die Abfrage ist in jedem Fall fail-open: Ist die Lebensmitteldatenbank nicht erreichbar, wird die Anfrage abgelehnt oder ist das Kontingent erschöpft, wird ein Scan trotzdem abgeschlossen und zeigt weiterhin Zahlen an. Diese Zahlen sind dann die eigene Schätzung der KI statt eines Werts aus der Datenbank, und die App weist auf dem Bildschirm darauf hin, statt den Unterschied unbemerkt zu lassen.

Vorschläge an die Lebensmitteldatenbank

Mit FOOD_DB_BACKFILL=true und einem Schlüssel leitet openplate jedes Lebensmittel, das jemand aus einem Foto oder einer eingetippten Mahlzeit speichert, über diesen Server an LowCarbCheck weiter:

  • Bei einem Lebensmittel, das die Person einem LowCarbCheck-Eintrag zugeordnet hat, werden die Namen des Lebensmittels in jeder App-Sprache übertragen, damit der Eintrag die fehlenden Bezeichnungen erhält.
  • Ein Lebensmittel ohne Treffer überträgt seine Namen in jeder App-Sprache und seine Makronährstoffe pro 100 g, damit LowCarbCheck es hinzufügen kann. Es benötigt einen englischen Namen sowie alle vier Angaben zu Kohlenhydraten, Fett, Eiweiß und Brennwert. Ein Lebensmittel ohne diese Angaben wird nicht übertragen.
  • Ein Name, den die Person selbst eingetippt oder geändert hat, wird niemals übertragen.

Ein Vorschlag enthält die Namen, die Makronährstoffe und die Information, ob das Lebensmittel aus einem Foto oder einer eingetippten Mahlzeit stammt. Er enthält kein Konto, keinen Tagebucheintrag, kein Foto und keine Adresse der Person. LowCarbCheck sieht nur deinen Server und deinen Schlüssel. LowCarbCheck bewertet jeden Vorschlag beim Eintreffen mit einem Modell und veröffentlicht, was die Prüfung besteht. Ein so veröffentlichtes Lebensmittel kommt aus der Lebensmitteldatenbank als Schätzwert gekennzeichnet zurück, und openplate zeigt und speichert es als solchen, niemals als kuratierte Quelle.

Jede Person kann dies für ihr Gerät in Einstellungen → KI abschalten. Auf jeder Instanz ist die Funktion deaktiviert, bis der Betreiber FOOD_DB_BACKFILL=true setzt.

Newsletter-Anmeldung

openplate wird ohne Mailingliste ausgeliefert. Wenn du sowohl NEWSLETTER_SUBSCRIBE_URL als auch NEWSLETTER_TURNSTILE_SITE_KEY setzt, fügt die Landingpage ein Anmeldeformular hinzu. Der Browser sendet es an den openplate-Server, welcher {email, locale, consent, source, turnstileToken} an deine URL weiterleitet, maximal fünf Anfragen pro Minute von einer Adresse. Der Browser erfährt diese URL nie, sie kann also in einem privaten Netzwerk liegen. Wenn du nur eines von beiden setzt, stoppt der Startvorgang. Lässt du beides ungesetzt, was der Standard ist, gibt es kein Formular, kein zusätzliches Skript und keine Änderung an der CSP.

Die Release-Prüfung

Der Server ruft https://openplate.de/latest.json alle sechs Stunden ab, und einmal etwa 90 Sekunden nach dem Start. Diese kleine Datei nennt die neueste Version. Der Server vergleicht diese Version mit seiner laufenden Version und meldet das Ergebnis unter Einstellungen > Über. Eine Schaltfläche „Jetzt prüfen“ löst ebenfalls eine Prüfung aus, begrenzt auf eine echte Anfrage pro Minute für die gesamte Instanz.

Der Server stellt die Anfrage, nicht der Browser. Wenn man openplate.de zur Produktions-connect-src hinzufügte, würde das die Positivliste erweitern, die verhindert, dass ein eingeschleustes Skript einen BYOK-Schlüssel entwendet. Die Anfrage ist ein einfacher GET-Aufruf ohne Body, Query-String, Token oder Instanzkennung. Wie jede HTTPS-Anfrage sendet sie die IP-Adresse des Servers. Sie sendet außerdem einen User-Agent im Format openplate/<version> (<platform>; <arch>), zum Beispiel openplate/1.2.3 (linux; arm64). Weitere Daten überträgt sie nicht. Das Projekt zählt die eindeutigen anfragenden Adressen pro Tag und speichert nur die Tagessummen. Proxy-Logs speichern IP-Adressen bis zu 15 Tage lang, genau wie bei normalen Website-Besuchen. Siehe ADR-0021.

bash
UPDATE_CHECK=off

Diese Einstellung deaktiviert Prüfungen vollständig. Der Server startet keinen Timer, sendet keine Anfragen, schließt die Instanz aus den Projektzählungen aus und vermerkt auf der Info-Seite, dass Prüfungen deaktiviert sind.

Die Prüfung meldet nur. openplate ist ein einzelner zustandsloser Container und kann sein eigenes Image nicht ersetzen, daher bleibt ein Upgrade genau das, was es immer war:

bash
docker compose -f compose.yml pull && docker compose -f compose.yml up -d

Die einzige Schaltfläche in der App, die tatsächlich etwas ändert, ist "Neu laden zum Aktualisieren". Sie erscheint, wenn der Server bereits einen neueren Build ausliefert, als die geöffnete Seite ausführt. Das lädt den Browser mit den Assets neu, die der Server hat, und ändert auf dem Host rein gar nichts.

Personen auf eine andere Instanz umziehen

Wenn du eine Instanz schließt und ihre Nutzer auf eine andere umziehen, lass den alten Container mit einer einzigen Einstellung weiterlaufen:

bash
MOVED_TO_URL=https://app.openplate.example

Jede Route auf der alten Instanz liefert dann eine einzige Hinweisseite aus. Sie nennt die neue Adresse von openplate, bietet eine Schaltfläche zur dortigen Anmeldeseite und weist Personen, die die App zum Startbildschirm hinzugefügt haben, an, dieses Icon zu entfernen und die neue Adresse hinzuzufügen. Die Seite wählt ihre Sprache in dieser Reihenfolge: die gespeicherte Auswahl des Nutzers, die vom Browser angeforderten Sprachen, dann DEFAULT_UI_LANGUAGE.

Die Seite informiert Nutzer darüber, dass ihr Konto und ihr Tagebuch mit ihnen umgezogen sind. Aktiviere diesen Modus nur, wenn das zutrifft: Die neue Instanz nutzt denselben Core-Server oder du hast die Konten dorthin migriert. Ein Tagebuch, das nur in einem Browser gespeichert ist, verbleibt in diesem Browser unter der alten Adresse; die neue Adresse kann es nicht lesen.

Eine HTTP-Weiterleitung kann diese Migration nicht abbilden. Ein Smartphone mit der installierten App führt einen Service-Worker aus, der Anwendungsseiten im Cache hält. Browser folgen Weiterleitungen nicht, wenn sie diesen Worker auf Aktualisierungen prüfen, sodass eine installierte App weiterhin ihre gespeicherte Kopie öffnen würde. In diesem Modus liefert /sw.js einen kleinen Worker aus, der alle Caches unter der alten Adresse löscht, sich selbst deregistriert und die Seite neu lädt. Die nächste Anfrage lädt den Hinweis dann direkt vom Server. Tagebuch-Daten, die im Browser gespeichert sind, lässt der Worker unberührt.

Die API gibt 410 Gone mit der neuen Adresse im Antwortkörper zurück. /healthcheck antwortet wie zuvor, und das Web-App-Manifest bleibt unverändert, sodass ein Startbildschirm-Icon weiterhin die alte Adresse öffnet und die Seite lädt. Lass diesen Modus aktiv, bis der Datenverkehr zur alten Adresse aufhört. Der Wert muss eine https://-Adresse auf einem anderen Host als APP_URL sein, ohne Benutzername oder Passwort; alles andere bricht den Start ab.

Die Content-Security-Policy

Sowohl der BYOK-Vision-Aufruf als auch der Schlüssel verbleiben vollständig im Browser, weshalb der Produktions-Build eine strikte Content-Security-Policy ausliefert. Deren connect-src erlaubt:

  • 'self'
  • Die eigenen Origins der integrierten Anbieter (OpenRouter, Mistral, Anthropic), automatisch aus dem Provider-Registry abgeleitet: siehe ADR-0007
  • localhost und 127.0.0.1 auf jedem Port. [::1] steht nicht auf der Liste, da eine CSP-Quelle keine IPv6-Adresse benennen kann; richte einen Client stattdessen auf localhost aus.
  • dein CORE_URL und DEFAULT_INFERENCE_BASE_URL, falls gesetzt
  • alles in CSP_CONNECT_EXTRA

Diese Erlaubnisliste verhindert, dass ein eingeschleustes Skript einen auf der Seite liegenden Schlüssel abzieht. Erweitere sie mit Bedacht.

Auf einer verwalteten Instanz ist der KI-Proxy der Core-Server, mit dem der Client ohnehin spricht, sodass sein Origin CORE_URL ist, der bereits in der Liste oben steht. Es gibt keinen zweiten Remote-Endpunkt freizugeben und nichts Zusätzliches, das dafür in CSP_CONNECT_EXTRA eingetragen werden müsste.

Analytik

openplate kann erfassen, wie es genutzt wird. Es erfasst nichts, bis du es konfigurierst, und es erfasst niemals, was du isst.

Setze MATOMO_URL und MATOMO_SITE_ID, um eine Instanz auf eine selbst betriebene Matomo-Installation zu verweisen. Lässt du beide ungesetzt (der Standard), lädt die Instanz kein Analytik-Skript, sendet keine Anfragen und liefert denselben Content-Security-Policy-Header aus wie vor der Existenz von Analytik. Setzt du nur einen der Werte, schlägt der Startvorgang absichtlich fehl: Für Betreibende ist die falsche Annahme, Analytik zu erfassen, schlimmer als eine Fehlermeldung.

Der Tracker läuft mit deaktivierten Cookies. Er speichert nichts auf dem Gerät, daher ist kein Einwilligungsbanner nötig.

Die Versionsprüfung läuft getrennt. Sie nutzt keine Tracker und kein Matomo. UPDATE_CHECK=off stoppt die Prüfung und die tägliche Zählung der Anfragen durch das Projekt. Siehe Die Release-Prüfung.

Was eine Stufe festlegt

MATOMO_EVENT_LEVEL bestimmt, wie viel die Instanz melden darf. Dies gilt nur, wenn Analytik bereits aktiv ist.

StufeWas erfasst wird
pageviewsNur Seitenaufrufe. Es wird niemals ein Funktionsereignis ausgelöst.
productSeitenaufrufe plus Softwarenutzung. Der Standardwert.
researchAlles, einschließlich Fasten, Gewicht, Weitergabe an Behandelnde und Teilnahme an Studien.

Nicht gesetzt bedeutet product. Ein unbekannter Wert bricht den Startvorgang ab. Eine konfigurierte Stufe ohne eingerichtetes Matomo bricht den Startvorgang ebenfalls ab, aus demselben Grund wie ein unvollständig konfiguriertes Wertepaar.

Was product erfasst

36 Ereignisse, die sich ausnahmslos auf die Software und nicht auf die Person beziehen.

BereichEreignisse
Onboardingabgeschlossen, Schritt abgeschlossen (Fokus, Gewicht, Körper), Schritt übersprungen
Scanerfolgreich, fehlgeschlagen (mit fester Fehlerkategorie), nichts gefunden, Modus ausgewählt, über geteiltes Foto gestartet
Tagebucherfasst (mit dem Eingabepfad: Suche, manuell, Teller-Scan, Etikett-Scan, Chip, Tag kopieren, erneut eintragen, gespeicherte Mahlzeit), Eintrag bearbeitet, Eintrag gelöscht, Eintrag wiederhergestellt, Mahlzeit gespeichert
Eigene Lebensmittelbearbeitet, gelöscht
KI-Anbieterverbunden (manuell, OAuth, Instanz-Voreinstellung), Schlüsselprüfung fehlgeschlagen, getrennt
Einstellungengeändert (Design oder Sprache)
Sicherungexportiert, importiert, CSV exportiert, Foto-Cache geleert
Kontoerstellt, gelöscht, Passwort geändert, Passwortzurücksetzung angefordert, Passwortzurücksetzung abgeschlossen, Einrichtung abgeschlossen
EinladungenLink eingefügt, Beitritt abgeschlossen
App-InstallationInstallationshinweis angezeigt, installiert, Offline-Seitenaufruf
StartseiteNewsletter abonniert, Handlungsaufforderung angeklickt

Was research hinzufügt

12 weitere Ereignisse. Jedes davon sagt etwas über die Gesundheit einer Person oder deren Teilnahme an einer Studie aus, weshalb sie standardmäßig deaktiviert sind, außer du forderst sie ausdrücklich an.

BereichEreignisse
FastenFasten gestartet (sofort oder geplant), Fasten beendet
Ziele und GewichtZiele gespeichert (Vorgaben oder Körperdaten), Gewicht erfasst
Freigabe für BehandelndeFreigabe erteilt, widerrufen, Schlüssel rotiert, Identität erstellt, geteiltes Tagebuch geöffnet
Forschungsstudieneingeschrieben, abgemeldet, Beitrag gesendet

Diese enthalten keine Werte. Ein Fastenereignis enthält keine Dauer, und ein Gewichtsereignis enthält kein Gewicht. Die Ereignisse haben jedoch Zeitstempel, wie jedes Analyseereignis, daher ergibt die Differenz aus Beginn und Ende eine Dauer, und ein Freigabeereignis verrät, dass die Person eine behandelnde Fachkraft hat. Die Studienteilnahme gehört zu den besonderen Kategorien personenbezogener Daten gemäß Art. 9 DSGVO.

Genau aus diesem Grund gibt es diese Stufe. Forschende, die eine Studie auf ihrer eigenen Instanz durchführen, benötigen diese Zahlen und dürfen sie von einwilligenden Teilnehmenden rechtmäßig erheben. Eine allgemeine Instanz sollte sie nicht erfassen und tut dies standardmäßig auch nicht.

Wenn du research aktivierst, gib dies in deiner eigenen Datenschutzerklärung an. Die Erklärung von openplate beschreibt die von openplate gehosteten Instanzen, nicht deine.

Was auf keiner Stufe gezählt wird

  • Jegliche Inhalte aus einem Tagebuch. Kein Lebensmittelname, kein Gewicht, kein Ziel, kein Foto, keine Mahlzeitenuhrzeit, keine Studien-ID.
  • Messwerte einer Person. Ereignisse enthalten eine feste Bezeichnung oder gar keine Daten.
  • Identifikatoren jeglicher Art. Keine Account-ID, keine E-Mail-Adresse, keine Geräte-ID.
  • Abfrage-Strings und URL-Fragmente, die vor dem Melden eines Seitenaufrufs vollständig entfernt werden. openplate legt dort Einmal-Tokens ab.
  • Bezeichner in einem Pfad. /diary/entry/<id> und /shared/<account id> werden durch einen Platzhalter ersetzt, bevor der Seitenaufruf gemeldet wird.

Die Regeln werden über Typen erzwungen statt durch ein Review. Jede Ereignisfunktion in app/lib/matomo-events.ts akzeptiert entweder nichts oder genau einen Wert aus einer festen Liste, sodass ein Lebensmittelname nicht ohne Kompilierfehler übergeben werden kann. tests/unit/no-telemetry-wiring.test.ts bricht den Build ab, wenn eine andere Datei direkt auf den Tracker zugreift oder wenn ein Matomo-Host oder eine Site-ID fest im Quellcode steht, wodurch eine selbstgehostete Instanz Daten an das Konto eines Fremden melden würde.

Die Entscheidung und ihre Begründung findest du in ADR-0010.

Managed Instances

Eine Instanz, die INSTANCE_MODE=managed setzt, ist eine Managed Instance: Ein Administrator lädt Personen per E-Mail ein, und jedes Konto enthält ein tägliches KI-Kontingent, sodass die Anmeldung einer Person in einem Schritt sowohl das Tagebuch als auch die KI bereitstellt. Die gehosteten Instanzen unter beta.openplate.de und app.openplate.de nutzen diesen Modus. Er ist standardmäßig deaktiviert: Ein Self-Hoster, der nichts einstellt, erhält die offene App.

INSTANCE_MODE=managed erfordert CORE_URL. Das Konto hält Tagebuch und Kontingent zusammen; das Deklarieren von managed ohne einen Core-Server bricht den Startvorgang ab, statt irgendetwas halb zu aktivieren.

Ein Administrator lädt Personen direkt aus der App heraus ein, unter /admin, oder über die Admin-API von openplate-core und ADMIN_TOKEN. Das allererste Konto, bevor ein Administrator existiert, stammt aus dieser API. self-hosting.md enthält den Befehl. Wenn Mail auf dem Core-Server konfiguriert ist, wird die Einladung per Mail versendet. Wenn kein Mail konfiguriert ist, enthält die Antwort den Link und du reichst ihn weiter. Ein vergessenes Passwort wird über einen Link zurückgesetzt. Dieser Link wird per Mail gesendet oder, ohne Mail, von einem Administrator erstellt (siehe self-hosting.md). Der Server verwahrt einen hinterlegten Wiederherstellungscode, der den Datenschlüssel nach dem Zurücksetzen entschlüsselt (siehe sync.md).

Eine verwaltete Instanz mit KI benötigt außerdem drei Werte auf dem Core-Server: UPSTREAM_BASE_URL und UPSTREAM_API_KEY (der Anbieter und sein Schlüssel) sowie ein Modell. Das Modell ist AI_ADVERTISED_MODEL, das Modell, das jeder Scan nutzt. Oder du setzt AI_TIERS_FILE=bundled, dann stammt das Modell aus der Tier-Datei des Core, ai-tiers.json, die das Modell, sein Routing und seinen Preis an einer geprüften Stelle bündelt (ein Pfad zu einer Datei, die du einbindest, funktioniert ebenfalls; siehe der KI-Proxy). Für Scans ist ein Modell erforderlich. Ohne ein Modell meldet der Core-Server kein Modell, und die App verweigert den Scan, statt ein Modell auf deine Rechnung zu wählen. Benenne das Modell so, wie dein Anbieter es tut, zum Beispiel vendor/model-name bei UPSTREAM_BASE_URL=https://openrouter.ai/api/v1, oder openplate-plate-1 vor openplate-inference. Mit einer Tier-Datei bleibt AI_ADVERTISED_MODEL nur als Notfall-Überschreibung für das Modell des Standard-Tiers erhalten, und der Core protokolliert bei jedem Start eine Warnung. Jedes Konto benötigt dann ein tägliches Kontingent, das bei 0 beginnt. Vergib es mit "dailyAiLimit", wenn du die Einladung erstellst, oder lege es später in /admin fest.

Was sich ändert, wenn die Option gesetzt ist:

  • /welcome bietet genau zwei Aktionen: Anmelden sowie Ich habe einen Einladungslink (übernimmt einen eingefügten Link und übergibt ihn an /join). Es gibt kein „Starten“.
  • /onboarding leitet für ein Gerät ohne lokales Tagebuch und ohne Konto auf /welcome um. Der anonyme, rein lokale Pfad ist geschlossen, nicht bloß ausgeblendet: Auf einer verwalteten Instanz führt er ins Leere, da es ohne Konto keine KI gibt und kein Tagebuch ohne Konto das Gerät überlebt. Ein Gerät, das bereits ein Tagebuch enthält, wird niemals abgewiesen.
  • /join führt eine einzige Zeremonie aus: Die Einladung wird direkt durch die Registrierungsanfrage in einer Transaktion eingelöst, und das Konto enthält ab diesem Moment sowohl das Tagebuch als auch das Kontingent. Die Aktion "Überspringen, ich habe bereits ein Konto" entfällt, da der Person auf einer solchen Instanz beides fehlt.
  • Einstellungen → Konto bietet abgemeldet die Anmeldung an und weist darauf hin, dass Konten hier über einen Einladungslink entstehen. Eine Schaltfläche "Konto erstellen" gibt es nicht.

Alles oben Genannte bleibt auf einer offenen Instanz (INSTANCE_MODE nicht gesetzt oder open) unverändert, und ein Test prüft beide Varianten nebeneinander.

Mitgliedereinladungen

Auf einer verwalteten Instanz ist ein Administrator nicht die einzige Person, die einladen kann. Drei Variablen von openplate-core bestimmen, ob ein gewöhnliches Mitglied jemanden einladen darf und zu welchen Bedingungen. Sie werden auf dem Core-Server gesetzt, nicht in der App. compose.core.yml und compose.full.yml leiten alle drei von .env an den Core-Server weiter.

VariableStandardwertBeschreibung
MEMBER_INVITE_DAILY_AI_LIMITnicht gesetzt (Einladungen aus)Wie viele KI-Anfragen pro UTC-Tag das eingeladene Konto erhält. Setze diesen Wert und MEMBER_INVITE_ALLOWANCE_DAYS zusammen oder keinen von beiden.
MEMBER_INVITE_ALLOWANCE_DAYSnicht gesetzt (Einladungen aus)Wie viele Tage nach der Registrierung dieses Kontingent gilt. Setze diesen Wert und MEMBER_INVITE_DAILY_AI_LIMIT zusammen oder keinen von beiden.
MEMBER_INVITE_LIFETIME_CAP5Wie viele Einladungen ein Mitglied insgesamt jemals versenden darf. Eine Ganzzahl von 0 oder höher. Erfordert, dass die beiden oberen Variablen gesetzt sind.

Die ersten beiden gelten ganz oder gar nicht. Wenn du nur eine davon setzt, bricht der Startvorgang ab und nennt die fehlende Variable. Wenn keine von beiden gesetzt ist, was der Standard ist, können Mitglieder niemanden einladen und POST /v1/auth/invites antwortet für alle mit 404. Du erstellst dann jede Einladung selbst.

Eine Einladung gewährt die Testphase. Die eingeladene Person erhält ein eigenes Konto und ein eigenes Tagebuch, dazu MEMBER_INVITE_DAILY_AI_LIMIT KI-Anfragen pro Tag für MEMBER_INVITE_ALLOWANCE_DAYS Tage nach der Registrierung. Sobald dieses Zeitfenster abgelaufen ist, antwortet der KI-Proxy mit 403. Das Tagebuch funktioniert weiterhin. Die Synchronisierung hängt niemals an einem Kontingent. Wer einlädt, bestimmt nichts davon, sondern sendet nur eine Adresse.

Die Obergrenze zählt Einladungsschreiben, nicht erfolgreiche Anmeldungen. Wenn du eine Einladung zurückziehst, wird sie dem Kontingent nicht wieder gutgeschrieben. Die Obergrenze gilt pro Konto, nicht pro Instanz. MEMBER_INVITE_LIFETIME_CAP=0 lässt die Route aktiv, gibt den Mitgliedern jedoch kein Guthaben zum Verbrauchen. Das unterscheidet sich davon, beide Variablen zu leeren und die Route ganz zu entfernen. Betrachte die Obergrenze gemeinsam mit AI_INSTANCE_DAILY_LIMIT. Das tägliche Kontingent oben multipliziert sich mit allen Mitgliedern der Instanz mal der Obergrenze, bevor es auf deiner Provider-Rechnung landet.

Administratoren sind ausgenommen. Die Obergrenze gilt nicht für sie, ebenso wenig wie die Regel, dass eine Adresse nach einer bereits genutzten Mitgliedereinladung keine zweite erhalten darf. Sie können über /admin so oft einladen, wie sie möchten.

Ein Mitglied sieht in der App, wie viele Einladungen noch übrig sind. Das steht in Einstellungen, Konto, unter Jemanden einladen. Dieser Abschnitt wird nur auf einer Instanz angezeigt, auf der die Funktion aktiv ist.

Eigene KI-Endpunkte

openplate-inference ist ein selbst gehosteter, OpenAI-kompatibler Endpunkt für Tellerfotos, den du mit Open-Weight-Modellen auf deiner eigenen Hardware betreibst, sodass niemand einen Cloud-AI-Schlüssel benötigt. Dieser Endpunkt, Ollama, vLLM, LM Studio oder jede andere Software, die das Chat-Completions-Protokoll von OpenAI spricht, verbindet sich auf dieselbe Weise: Füge unter Einstellungen → KI einen openai-compatible-Anbieter hinzu und gib ihm deine Basis-URL.

  • Ein lokaler Endpunkt auf demselben Rechner (http://localhost:11434/v1 und ähnliche) erfordert keine Konfiguration: Die Loopback-Ausnahme deckt dies bereits ab.
  • Ein entfernter Endpunkt (ein anderer Rechner in deinem LAN oder ein von dir betriebener Inferenz-Server) wird von der CSP standardmäßig blockiert. Füge dessen Origin hinzu und starte die App neu:
    bash
    echo "CSP_CONNECT_EXTRA=https://ai.example.com" >> .env
    docker compose -f compose.yml up -d
  • api.openai.com ist über einen Browser nie erreichbar: Die API von OpenAI blockiert Cross-Origin-Anfragen. Leite OpenAI-Modelle stattdessen über OpenRouter weiter, unabhängig von CSP_CONNECT_EXTRA.

Von der Instanz bereitgestellte KI

Statt von jedem Besucher einen eigenen Schlüssel zu verlangen, kann eine Instanz ihren eigenen Endpunkt bereitstellen.

Bevor du DEFAULT_INFERENCE_API_KEY setzt, beachte, dass der Wert öffentlich ist: Er ist im HTML der Seite eingebettet und über den Seitenquelltext für jeden lesbar, der die App öffnen kann. Die vollständige Regel ist der zweite Punkt unten. Lies ihn zuerst.

Setze DEFAULT_INFERENCE_BASE_URL (plus DEFAULT_INFERENCE_MODEL, und DEFAULT_INFERENCE_API_KEY, falls der Endpunkt einen erfordert), und die Seite mit den KI-Einstellungen sowie der Scan-Bildschirm erhalten eine 1-Klick-Verbindung namens "Dieses openplate stellt eine eigene KI bereit". Bleibt die Variable ungesetzt, bleibt die Eingabe eines eigenen Schlüssels der einzige Weg; es wird nichts zusätzlich gerendert und nichts zusätzlich an den Browser gesendet.

Drei Regeln:

  • Es muss eine Adresse sein, die ein BROWSER erreichen kann. Das Foto wandert vom Gerät direkt zum Endpunkt, niemals über den openplate-Server, daher funktioniert ein Compose-Hostname wie http://openplate-inference:8080/v1 nicht. Veröffentliche den Endpunkt oder platziere ihn hinter deinem Reverse-Proxy. Dessen Origin wird automatisch zur CSP hinzugefügt.
  • DEFAULT_INFERENCE_MODEL muss ein Modell bezeichnen, das dein Endpunkt tatsächlich bereitstellt. Der Standardwert openplate-plate-1 ist die ID, die openplate-inference bereitstellt, und er wird unverändert gesendet. Wenn du die Basis-URL stattdessen auf Ollama, vLLM oder LM Studio richtest, musst du diesen Wert auf den Namen setzen, den diese Laufzeitumgebung selbst bereitstellt, andernfalls schlägt jede Anfrage fehl.
  • DEFAULT_INFERENCE_API_KEY ist öffentlich. Er wird nicht auf dem Server gespeichert: Er ist im HTML der Seite eingebettet und über den Seitenquelltext für jeden lesbar, der die App öffnen kann. Für einen Endpunkt, den nur dein Haushalt oder dein Tailnet erreichen kann, ist das in Ordnung. Für den Schlüssel eines abgerechneten Cloud-Anbieters ist es nicht in Ordnung, und auf einer Instanz, die ohne VPN, Tailnet oder Auth-Proxy im offenen Internet steht, ist es nicht in Ordnung. Wenn dein Endpunkt keinen Schlüssel benötigt, lasse ihn ungesetzt.

Ein fehlerhafter Wert für DEFAULT_INFERENCE_BASE_URL lässt den Start absichtlich fehlschlagen, damit ein Tippfehler nicht so aussieht, als sei "die Schaltfläche einfach nie erschienen".

Diese Instanz-Voreinstellung (connectedVia: 'preset') legt die betreibende Person dieser Instanz für alle Besucher fest. connectedVia: 'invite' ist ein verwandter, veralteter Wert: Er kennzeichnete eine KI-Einstellungszeile aus dem alten Einladungsablauf von openplate-gateway, der in M192 entfernt wurde. Eine Managed Instance schreibt diese Zeile überhaupt nicht mehr: Das Konto selbst trägt das Kontingent, und der KI-Proxy wird über CORE_URL erreicht, nicht über einen separaten Eintrag in den Einstellungen.

Verbindung mit OpenRouter

Einstellungen → KI → Mit OpenRouter verbinden ist ein browserbasierter Ein-Klick-OAuth-Ablauf (PKCE): kein Schlüssel zum Kopieren und Einfügen. Er leitet diesen Tab zur Freigabeseite von OpenRouter und zurück; bestätige die Freigabe, und der ausgestellte Schlüssel landet direkt im lokalen Speicher dieses Browsers. Der openplate-Server ist an dieser Schleife nicht beteiligt: Er sieht, speichert oder leitet den Schlüssel zu keinem Zeitpunkt weiter.

  • Lege direkt ein Ausgabenlimit fest. Die Zustimmungsseite von OpenRouter bietet neben der Kontoauswahl ein frei wählbares Ausgabenlimit (optional, mit Rücksetzintervall). Es begrenzt den Betrag, den der verknüpfte Schlüssel jemals ausgeben kann, unabhängig von openplate.
  • Das Standardmodell ist google/gemini-3.5-flash-lite, ein kostenpflichtiges Modell, etwa 0,001 $ pro Scan. Es wurde gegenüber jedem :free Modell bevorzugt, da die :free Endpunkte von OpenRouter erst erreichbar werden, nachdem du auf Kontoebene die Optionen „Darf mit Anfragedaten trainieren“ / „Darf Prompts veröffentlichen“ dieses Anbieters aktiviert hast, und einige kostenlose Vision-Modelle Prompts über Wochen speichern. Du kannst weiterhin selbst ein :free Modell aus der Modellliste wählen, als explizites, offengelegtes Opt-in.
  • Die manuelle Schlüsseleingabe funktioniert bei jedem Anbieter, einschließlich OpenRouter, über das Feld „API-Schlüssel manuell einfügen“.
  • Der verknüpfte Schlüssel erscheint in deinen OpenRouter-Schlüsseleinstellungen mit der Bezeichnung „Eine App“. Das Trennen in openplate löscht den Schlüssel nur von diesem Gerät: Es widerruft ihn nicht bei OpenRouter. Widerrufe ihn auf dieser Seite selbst.
  • Gleiche Garantie wie bei jedem anderen Schlüssel: nur im lokalen Speicher des Geräts gesichert, vom JSON-Backup/-Export ausgenommen und nie an den openplate-Server gesendet.

Es funktioniert von jedem sicheren Ursprung aus. Die OAuth-Callback-URL wird zum Zeitpunkt der Anfrage aus window.location.origin abgeleitet. Sie ist weder fest einprogrammiert noch vorab bei OpenRouter registriert. Die Schaltfläche funktioniert ohne Änderungen auf http://localhost:3000 oder auf deiner eigenen https://-Domain. Auf einer einfachen http://-LAN-Adresse schlägt sie fehl, da sie ihren Einmalcode mit der Web Crypto API hasht, die Browser dort deaktivieren. Füge stattdessen einen Schlüssel manuell ein, oder siehe self-hosting.md.

Ein Hinweis, falls du hinter einem Reverse Proxy hostest, der Anfrage-URLs protokolliert: Die Callback-URL (einschließlich ihres einmaligen Parameters state) erscheint wie jede andere URL in deinen Zugriffsprotokollen. Sie ist kein Geheimnis (sie zu kennen gewährt niemandem Zugriff auf einen Schlüssel), aber bereinige sie, falls deine Protokollaufbewahrung ein Problem darstellt.

Diese Seite auf GitHub bearbeiten