L'app
Architettura
I tre programmi e il database degli alimenti, cosa contiene ciascuno e come si compongono
Questa pagina è tradotta automaticamente dalla documentazione in inglese.
Tre programmi, un prodotto e due componenti opzionali, più un servizio esterno che risponde ai nomi degli alimenti. Questa pagina spiega cosa contiene ciascuno di essi, e quale si trova sul percorso dei tuoi dati.
Il disegno qui sotto mostra l'intero sistema in cinque frecce. Il tuo dispositivo conserva il diario e la foto del piatto. Il diario esce cifrato, diretto a openplate-core. La foto esce verso l'endpoint IA che hai configurato. Il server dell'app invia la pagina, e inoltra a un database alimentare i nomi degli alimenti che cerchi. Non si trova né sul percorso del diario né su quello della foto.
Sorgente del diagramma
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"]Dietro "il tuo endpoint AI" possono esserci tre cose: un fornitore cloud di cui possiedi una chiave, una macchina openplate-inference sul tuo hardware o, su un'istanza gestita, il server centrale stesso, che inoltra la foto e la scala dalla tua quota. topologies.md illustra tutti e quattro i modi di eseguire openplate, con una piccola immagine per ciascuno.
Il client è il prodotto
Tutto ciò che appartiene a un utente (registri dei pasti, pesi, cibi personali, obiettivi, impostazioni dell'IA) viene scritto nell'IndexedDB del browser sul dispositivo in cui è stato inserito (app/lib/local-store/). Lì viene salvato in chiaro, perché è il tuo dispositivo, e non lo lascia mai se non nelle due forme che scegli: un'esportazione JSON che scarichi, oppure un blob cifrato di sincronizzazione.
Il server dell'app è un unico container stateless. Nessun database, nessun ORM, nessuna migrazione, e nessun secret richiesto per l'avvio. Può contenere esattamente un secret opzionale, la chiave del gestore per il database alimentare, descritta sotto. Distruggere il container non fa perdere nulla. Non si tratta di parsimonia, è l'intera promessa: vedi ADR-0006.
La sincronizzazione è identità, a fianco del percorso della foto e mai al suo interno
openplate-core sposta un diario tra i dispositivi. È l'unico servizio in openplate che gestisce gli account. È un componente distribuibile a parte con la propria immagine, il proprio database e il proprio segreto, e il browser ci comunica direttamente. Il server dell'app non fa da proxy per suo conto e non gestisce alcuna route di sincronizzazione.
Il diario viene cifrato prima di lasciare il dispositivo. Il client serializza l'archivio locale, lo comprime con gzip, lo cifra con AES-256-GCM sotto una data key casuale, e carica il risultato come un unico blob opaco. La data key viene protetta con un wrapping sotto una chiave derivata dalla tua passphrase: la passphrase viene estesa con Argon2id e suddivisa tramite HKDF in rami indipendenti. Due di questi rimangono sul dispositivo e sbloccano ciò che ti appartiene, mentre un terzo viene inviato come credenziale di accesso. Sono elementi fratelli, non genitore e figlio, quindi possedere la credenziale non rivela nulla sulla chiave.
Il gestore conserva una chiave di recupero. Al momento della registrazione l'app genera un codice di recupero e protegge la data key con un wrapping sotto di esso. Invia il codice a openplate-core, che lo sigilla sotto un proprio secret. Questo è ciò che permette al ripristino della password via email di restituirti il tuo diario invece di un account vuoto. Significa anche che il gestore di un'istanza può ripristinare, e in linea di principio leggere, un diario presente su di essa. Su un'istanza che ospiti tu stesso, quel gestore sei tu. sync.md illustra il compromesso per intero.
Ciò che il server vede oltre al testo cifrato è descritto chiaramente in PROTOCOL.md §9: un indirizzo email, la dimensione del blob, frequenza e orari di scrittura, numeri di versione, e parametri KDF.
La console di studio opzionale gestisce i propri account separati sullo stesso server. Sincronizzazione indica quando è attiva, e ADR-0008 spiega perché risiede nell'app.
L'inferenza è calcolo, e la foto ci arriva direttamente
Una foto del piatto viene letta nel browser e inviata direttamente all'endpoint compatibile con OpenAI che hai configurato. Il server openplate non fa mai parte di quella richiesta. Non viene caricata qui, non viene scritta su disco, non viene registrata nei log. Vengono salvati solo i numeri risultanti, nell'archivio locale del dispositivo; la foto rimane sul dispositivo che l'ha scattata, esclusa sia dalle esportazioni JSON sia dai payload di sincronizzazione.
Quell'endpoint è un provider cloud che paghi (il percorso BYOK) oppure il tuo container openplate-inference. Nel caso self-hosted il modello individua i cibi sul piatto e stima i grammi, poi i macronutrienti vengono cercati, non inventati: carboidrati, proteine, grassi e kcal vengono ricavati per nome dalla fonte alimentare configurata, per impostazione predefinita un estratto integrato di USDA FoodData Central (8.041 cibi generici inclusi nell'immagine, nessuna chiamata di rete, pubblico dominio). Il modello linguistico non inventa mai i valori dei macronutrienti.
Dato che è il browser a effettuare la chiamata, l'endpoint deve essere un indirizzo raggiungibile da un browser. Un hostname di compose come http://inference:8300/v1 non funzionerà, anche se i due container possono comunicare tra loro in quel modo. Usa l'indirizzo LAN dell'host, un nome della tailnet, o un hostname sul tuo reverse proxy.
Il database alimentare è una ricerca per nome, attraverso il server dell'app
Un modello gestito o nel cloud restituisce una propria stima dei macro per ogni alimento rilevato. L'app verifica poi quegli alimenti confrontandoli con un database curato. Il browser invia solo i nomi rilevati dal modello a /api/food-matches del server dell'app. Il server cerca ciascun nome su LowCarbCheck (FOOD_DB_API_URL). Una corrispondenza può sostituire la stima del modello nella schermata di conferma. La ricerca nella schermata Aggiungi e i valori di riferimento nella schermata Nutrienti passano attraverso la stessa ricerca lato server.
Questo è l'unico percorso su cui si trova il server dell'app, ed è stretto per costruzione:
- Trasporta nomi degli alimenti, nomi dei nutrienti e la lingua della schermata. Mai una foto, mai la tua chiave IA, mai una voce del diario.
- LowCarbCheck vede l'indirizzo del server dell'app e la chiave dell'istanza, mai il tuo indirizzo. Ogni persona su un'istanza condivide quella chiave e la sua quota.
- Il server tiene in cache le risposte, e solo una ricerca che manca la cache incide sul limite di richieste per indirizzo.
- In caso di errore fallisce aprendosi. Se il database non è raggiungibile, rifiuta la chiave, o esaurisce la quota, la scansione si completa comunque con i valori del modello, e la schermata lo segnala.
FOOD_DB_API_URL=""lo disattiva, e a quel punto nessun nome di alimento lascia il tuo server.- Con
FOOD_DB_BACKFILL=trueinclude anche le proposte: i nomi di un alimento salvato da una risposta dell'IA, in tutte le lingue dell'app, e per gli alimenti senza corrispondenza i macronutrienti per 100 g. Non include mai un nome digitato dall'utente, né una foto o una voce del diario. Consulta configuration.md.
Viene eseguito sul server anziché nel browser. Questo tiene la chiave fuori dalla pagina, e consente alla configurazione del gestore di decidere se i nomi debbano uscire. La chiave è FOOD_DB_API_KEY. In sua assenza l'istanza usa il livello anonimo di LowCarbCheck; configuration.md elenca i livelli.
Il server centrale gestisce la multiutenza e si posiziona davanti al motore di calcolo su un'istanza gestita
Un'istanza può impostare INSTANCE_MODE=managed (vedi configuration.md). Questo dichiara una sola cosa: un'organizzazione gestisce questa istanza, invita i propri utenti via email, e assegna a ciascuno una quota giornaliera di IA. openplate-core è ciò che gestisce questo aspetto, l'account che già mantiene per la sincronizzazione contiene anche la quota, quindi non ci sono ulteriori passaggi di connessione né seconde credenziali.
Per il browser non cambia nulla: un account che ha effettuato l'accesso con una quota esegue le scansioni tramite il proxy AI esposto da openplate-core, lo stesso servizio con cui il client parla già per la sincronizzazione. Verso ciò che sta dietro, openplate-core si comporta da client: punta a un fornitore cloud o al tuo container openplate-inference. L'inferenza è il livello di calcolo, openplate-core è il livello multi-tenant su un'istanza gestita, e i due si compongono: il server centrale non ospita modelli e non risponde a nessuna scansione da solo.
Si trova sul percorso delle foto, che ne costituisce il costo reale, e la mitigazione è una caratteristica del codice anziché un'impostazione: il tipo di campo del logger accetta solo tipi primitivi, quindi un body non può finire in una riga di log, e le stringhe di errore a monte vengono ripulite prima di essere registrate o restituite. I membri di un'organizzazione condividono la spesa, non i dati; una foto del piatto che raggiunge il proxy viene letta una sola volta e non memorizzata.
La quota conta le richieste, non il denaro. Un gestore può anche impostare un limite giornaliero per l'intera istanza (AI_INSTANCE_DAILY_LIMIT), e rimane comunque necessario un limite di spesa sulla chiave a monte presso il provider.
Un amministratore gestisce l'istanza da /admin nell'app: persone e relative quote, inviti, attività, stime registrate, e quali valori di riferimento la schermata Nutrienti riporta.
Cronologia
Da agosto a settembre 2026 questo era un servizio separato, openplate-gateway: un piccolo proxy compatibile con OpenAI che conteneva una singola chiave a monte e rilasciava a ogni membro un token opk_… con la propria quota giornaliera. Con M192 (settembre 2026) è stato integrato in openplate-core: ora un unico account gestisce sia il diario sia la quota, eliminando così il secondo servizio, il secondo link di invito e la seconda credenziale da distribuire.
Cos'altro può trasportare openplate-core
Ciascuna funzionalità qui sotto è disattivata per impostazione predefinita, e ciascuna modifica ciò che il server conserva. Il file README di openplate-core descrive ciascuna di esse.
- Condivisione di un diario con un medico (
SYNC_SHARING=true). Il proprietario protegge la data key con un terzo wrapping, sotto la chiave pubblica del medico, e il server memorizza tale chiave protetta. Il browser del medico la sblocca e legge il diario all'indirizzo/shared. La condivisione non dà al server nulla di nuovo da aprire. - Contributi alla ricerca (
SYNC_RESEARCH=true). Una persona si iscrive a uno studio tramite un link e invia i totali giornalieri sotto uno pseudonimo. I totali sono sigillati per lo studio, ma il server apprende quale account contribuisce a quale studio. - Stime segnalate (
SYNC_FEEDBACK=true). "Segnala una stima errata" invia la foto, i numeri e un record di consenso al server, dove un amministratore li esamina su/admin. A differenza di una scansione, quella foto viene memorizzata. - Il battito non richiede alcuna impostazione dell'operatore: ogni persona lo attiva sotto Impostazioni, Condivisione. Invia conteggi arrotondati. Un pasto conta una volta con le sue calorie arrotondate a 50 e le sue proteine a 5 g. Una scansione conta una volta, e un digiuno in corso invia un heartbeat. La schermata iniziale mostra il totale dell'istanza per la giornata di oggi. Il server conserva i totali giornalieri e un registro di chi ha contribuito ogni giorno, per 30 giorni.
- Notifiche push (
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT). Il server memorizza una sola sottoscrizione per dispositivo: l'indirizzo push del browser e le sue chiavi, un fuso orario, una lingua e le impostazioni dei promemoria. Invia a ogni dispositivo al massimo due notifiche al giorno, e ciascuna include solo il proprio tipo, mai testo che ti riguarda. Il servizio push del tuo browser ne effettua la consegna. - Piani a pagamento (
PLANS_UPSTREAM_URL,PLANS_UPSTREAM_SECRET). openplate-core inoltra/v1/plans/*a un servizio dei piani gestito dall'operatore, insieme all'id dell'account e all'indirizzo email. L'app mostra Impostazioni, Piano solo quando il server segnala che i piani sono attivi.
BYOK è la soluzione a zero server
Senza alcun container di inferenza, il browser contatta direttamente un provider cloud usando la chiave che hai inserito su quel dispositivo. La chiave viene memorizzata nello storage locale del dispositivo, esclusa dall'esportazione JSON e non viene mai inviata al server di openplate: non esiste alcuna copia sul server, né cifrata né di altro tipo, e non c'è alcun proxy della chiamata lato server.
La Content-Security-Policy di produzione è parte integrante di questa garanzia e non un semplice dettaglio formale: la allowlist per connect-src deriva dal registro dei provider, ed è ciò che impedisce a uno script iniettato di esfiltrare una chiave presente nella pagina. Vedi configuration.md.
Chi conserva cosa
| Componente | Cosa memorizza | Cosa vede in transito |
|---|---|---|
| Il tuo browser | L'intero diario, in chiaro, in IndexedDB. La tua chiave per l'IA. Le foto del piatto nella cache. | Tutto. È il tuo dispositivo. |
| server dell'app openplate | Nessun database, nessun account, nessun diario. Al massimo un segreto: la chiave dell'operatore per il database degli alimenti. | Richieste di pagine, e i nomi degli alimenti che cerchi o scansioni, che vengono inoltrati al database degli alimenti. Mai una foto, mai la tua chiave per l'IA, mai una voce del diario, mai un blob di sincronizzazione. |
| openplate-core (opzionale) | Un indirizzo email, un verificatore di autenticazione, parametri KDF, il diario come testo cifrato, e il codice di recupero depositato che può decifrarlo. Su un'istanza gestita, anche la quota giornaliera di ogni account e il conteggio di utilizzo. Con le funzionalità sopra elencate attive, anche ciò che ciascuna di esse elenca. | Dimensione del blob, orari di scrittura, metadati di sessione. Su un'istanza gestita, anche la foto inoltrata al proxy IA, per il tempo necessario a inoltrarla, letta una volta sola, non memorizzata. |
| openplate-inference (opzionale) | Nulla per utente: nessun account, nessuna sessione, nessun cookie. I pesi del modello e un dataset alimentare. | La foto che hai inviato, per la durata della richiesta. Con la fonte alimentare predefinita non effettua alcuna chiamata in uscita tranne il download una tantum dei pesi. Con FOOD_SOURCE=lcc o off invia all'esterno i nomi degli alimenti, mai la foto. |
| database alimentare LowCarbCheck (attivo se non disattivato) | Un conteggio di utilizzo per chiave, oppure per indirizzo di rete per un chiamante che non ne ha una. | Nomi di alimenti e una lingua, dal server dell'app, con la chiave dell'istanza. Mai una foto, e mai la tua identità. |
| Provider IA su cloud (percorso BYOK) | Qualunque cosa sia indicata nella loro policy. | La foto e la tua chiave. Si applicano i loro termini, non i nostri. |