Il server centrale
openplate-core trasferisce il tuo diario tra i tuoi dispositivi. Memorizza un indirizzo email e una copia cifrata del tuo diario. Conserva anche il tuo codice di recupero, sigillato con un proprio segreto, così se dimentichi la password puoi recuperare il diario. Lo stesso codice consente al gestore di un'istanza di aprire un diario presente su di essa.
Cosa può e non può leggere
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.
Cosa sa
Massima trasparenza sui metadati, perché "cifrato end-to-end" viene spesso inteso come "il server non sa nulla":
- Dimensione del blob, e quindi un'approssimazione di quanti dati contiene l'account. La compressione rende questo segnale più sfumato rispetto a prima, non nascosto.
- Frequenza e tempistica di scrittura: quando un dispositivo sincronizza e con quale frequenza.
- Numeri di versione:
blobVersion,envelopeVersione il numero di versioni conservate. - Parametri KDF e salt per il record della passphrase. Non sono segreti, esistono per essere forniti a un nuovo dispositivo prima del login.
- Se un account ha completato la configurazione (ha record di chiavi) e se ha mai sincronizzato (ha un blob).
- L'account stesso: un indirizzo email, un nome visualizzato facoltativo, un ruolo, una quota giornaliera di IA, un istante di sospensione, un verificatore di autenticazione (un hash con chiave di un hash con chiave della passphrase, vedi §5.8), un secondo verificatore con la stessa struttura sulla prova di recupero e i parametri KDF dell'account. L'indirizzo identifica una persona reale, una categoria di dati personali che la versione 0.5.0 aveva rimosso e che la 0.6.0 ha deliberatamente reintrodotto (ADR-0005): i membri di un'organizzazione vengono identificati dall'indirizzo a cui è arrivato il loro invito, perché è l'identificatore che ricorderanno ancora tra un mese.
- Il CODICE DI RECUPERO dell'account, sigillato (
accounts.recovery_code_escrow, §3.1). Questa è la voce dell'elenco su cui chi legge dovrebbe soffermarsi. È AES-256-GCM con una sottochiave diSERVER_SECRET, quindi un dump del database da solo non basta per aprirlo, ma il gestore di un'istanza gestita ha entrambi. Il gestore di un'istanza gestita può aprire qualsiasi account presente su di essa. Non tramite un endpoint e non tramite un percorso di codice di questo servizio, ma leggendo quella colonna con il segreto alla mano ed eseguendo la HKDF del client stesso. Un'istanza in self-hosting è gestita da te, quindi lì la promessa originale resta valida. Decidere se fidarsi di un'istanza ospitata equivale quindi a decidere se fidarsi del suo gestore. - Inviti in sospeso: per ciascuno, un indirizzo, un nome facoltativo, un ruolo e una quota, appartenenti a qualcuno che NON ha ancora un account e non ha fornito alcun consenso. La creazione è un'azione dell'operatore, e
DELETE /v1/admin/invites/:idritira la riga. Una riga generata dalla porta di richiesta del §5.8.3 viene contrassegnata come tale, così che un operatore possa contarle. Un invito completato perde l'indirizzo e il nome entro un'ora: revocata o scaduta su ogni istanza, riscossa su un'istanza conTRIAL_ADDRESS_PEPPER, che conserva solo l'hash con chiave descritto sotto. Senza il pepper una riga riscossa mantiene il proprio indirizzo, poiché la regola di reinvito dei membri del §5.21 lo legge. - La prova delle scansioni, su un'istanza che ne esegue una: le scansioni gratuite concesse e usate, due interi sulla riga dell'account. Solo per un account con prova di scansione, una riga per azione dell'IA: un id opaco scelto dal client, un orario, un conteggio delle richieste e se la risposta sia stata recapitata o meno, conservato 24 ore e poi eliminata, e mai registrata nei log. Ogni riga di invito riporta un hash unidirezionale con chiave della relativa casella di posta (HMAC-SHA256 sotto
TRIAL_ADDRESS_PEPPER, un segreto detenuto solo dall'operatore, sulla chiave di prova del §5.8.3), mai una seconda copia dell'indirizzo. - Dopo l'eliminazione di un account, su un'istanza che prevede una prova delle scansioni: l'indirizzo e il nome vengono rimossi da ogni riga di invito relativa a quella casella di posta e, solo se l'account disponeva di una prova, viene conservato un solo hash con chiave della casella di posta, e nient'altro: nessun nome, nessun id e una sola data, l'istante dell'eliminazione, che permette di chiudere la riga. Questo impedisce alla stessa casella di posta di ottenere una seconda prova. Senza il segreto dell'operatore l'hash non può essere decifrato né confrontato con un elenco di indirizzi. Una procedura di pulizia lo elimina
TRIAL_HASH_RETENTION_DAYS(365 per impostazione predefinita) dopo quell'istante, in base alla base giuridica dell'Art. 6(1)(f) GDPR (legittimo interesse a prevenire l'abuso delle scansioni gratuite, non esaminato da un legale, ADR-0010). Un'istanza che non concede prove delle scansioni non conserva alcun hash. Su un'istanza senza prova delle scansioni, le righe di invito mantengono il loro indirizzo dopo l'eliminazione, in conformità alla regola di reinvito dei membri del §5.21. - Utilizzo dell'IA: un intero per account per giorno UTC, conservato per 90 giorni e poi eliminato (§5.20). Un conteggio, mai un log: nessun prompt, nessuna risposta, nessun modello, nessun timestamp oltre al giorno. Un gestore può leggere i contatori di un account come una sequenza giorno per giorno (
GET /v1/admin/accounts/:id/activity), che costituisce un metadato su quando una persona ha usato un'app per la salute ed è limitato esattamente per questo motivo. - Il polso della community, per gli account che lo hanno abilitato (§5.23, ADR-0007): totali giornalieri a livello di istanza per pasti, fotografie, calorie e grammi di proteine, una riga per account contributore al giorno e una riga di presenza a breve termine che indica che un account sta digiunando in questo momento. I totali non sono riconducibili a nessuno, mentre la riga del contributore e la riga di presenza lo sono, e indicano solo "questo account ha contribuito oggi" e "questo account sta digiunando". I totali giornalieri e le righe dei contributori vengono conservato per 30 giorni, la presenza scade 30 minuti dopo l'ultimo heartbeat e le route non registrano alcun id account. Chi non lo ha mai attivato non invia nulla e non compare in nessuna parte di questi dati.
- Un'iscrizione push, per un dispositivo il cui proprietario ha attivato le notifiche (§5.24, ADR-0008): l'endpoint del servizio push, le due chiavi con cui cifra, una stringa di user agent limitata, un fuso orario IANA, una lingua, il minuto del giorno locale in cui è previsto un recupero, l'ultimo giorno locale in cui ne è partito uno, il giorno locale in cui il dispositivo è stato visto l'ultima volta, l'istante in cui ha chiesto di essere risvegliato e il conteggio di quanto inviato oggi. Nel loro insieme, questi dati indicano indicativamente quando questa persona è sveglia, indicativamente dove si trova nel mondo e, tramite
wake_at, quando termina un suo digiuno. Quest'ultimo elemento coincide con la riga di presenza del battito, che indica che lo stesso digiuno è in corso; ADR-0008 dichiara esplicitamente la correlazione invece di lasciarla scoprire. Ciò che NON viene memorizzato è una singola parola del testo di una notifica: ogni notifica push trasporta una tipologia. La riga scompare quando il dispositivo annulla l'iscrizione, quando il servizio push lo disconosce o insieme all'account. - Un consenso per i dati sanitari, su un'istanza che ne richiede uno (§5.15.1): la versione del testo accettata dalla persona e l'istante in cui questo servizio l'ha registrata, due colonne nella riga dell'account. Attesta che la persona usa un'app per la salute e ha acconsentito al trattamento di tali dati da parte dell'operatore, il quale deve essere in grado di dimostrarlo. È visibile a un operatore (§5.20), nessuna route lo cancella, e viene rimosso insieme alla riga dell'account all'eliminazione.
- L'ultima volta che una persona ha fatto qualcosa:
accounts.last_seen_at, scritto a ogni accesso e da un completamento tramite proxy, e deliberatamente non da un rinnovo del token o da una richiesta periodica di sincronizzazione, quindi significa "qualcuno ha agito" anziché "un client era in esecuzione". È visibile a un operatore (§5.20) e viene eliminato insieme alla riga dell'account. - Dichiarazioni di legge (
POST /v1/legal/declarations, una cancellazione o un recesso, su ogni istanza): il nome, l'indirizzo, il riferimento del contratto, il motivo e le date inserite dalla persona, l'orario di arrivo e l'eventuale account corrispondente. Viene conservato conservato fino alla fine del terzo anno solare successivo all'anno di ricezione, calcolato secondo il fuso orario Europe/Berlin, e poi cancellato dalla pulizia oraria: una dichiarazione ricevuta il 2026-09-21 viene eliminata a partire dal 2030-01-01 alle 00:00 a Berlino. L'eliminazione dell'account non lo cancella prima; la riga perde il suo identificativo account ma rimane memorizzata, poiché costituisce la registrazione di quanto dichiarato dalla persona. L'invio delle ricevute per email è limitato a tre per indirizzo normalizzato, aLEGAL_DECLARATION_RECEIPTS_PER_NETWORK_PER_DAY(10 per impostazione predefinita) per rete mittente e aLEGAL_DECLARATION_RECEIPTS_PER_DAY(200 per impostazione predefinita) per istanza. Ogni limite si applica a qualsiasi intervallo di 24 ore a ritroso. I totali vengono calcolati a partire da queste righe, tranne il conteggio di rete, che risiede in memoria. Una dichiarazione che supera la soglia viene comunque memorizzata, inoltrata e inviata all'operatore, e il202resta lo stesso. La ricevuta non ripete mai il nome, il riferimento del contratto o il motivo; soltanto la copia dell'operatore li contiene. - Metadati di sessione: quante sessioni attive esistono, quando ciascuna è stata creata e quando i token sono stati ruotati o revocati l'ultima volta. I valori dei token stessi sono memorizzati solo come digest.
- Il grafo degli studi, su un'installazione con
SYNC_RESEARCHimpostato (§5.18): quale account contribuisce a quale studio, quando, con quale frequenza e quanto è grande ciascun contributo. Un arco qui indica che "i dati sanitari di questa persona sono nello studio Y", cioè dati personali legati alla salute della stessa classe dell'arco di assistenza descritto sotto. È inevitabile, e la prova è la revoca: cancellare la riga di un contributore richiede di individuarla, l'eliminazione dell'account deve propagarsi a cascata attraverso di essa, e sia il compare-and-swap sia il controllo degli abusi usano come chiave l'account. Uno schema che rendesse cieco il server ne romperebbe uno e l'analisi del traffico lo smaschererebbe comunque, quindi questo dato viene dichiarato anziché evitato a metà. Il ricercatore non riceve mai la mappatura (§5.18 non trasporta alcun id account), la revoca elimina definitivamente l'arco lasciando solo uno pseudonimo, e un'installazione senza questo flag non ha alcuna tabella per ospitare un grafo degli studi. - Il grafo di condivisione, su un'installazione con
SYNC_SHARINGimpostato (§5.16): quale account ha concesso l'accesso in lettura a quale altro account, quando la concessione è stata effettuata e quando il beneficiario la esercita. Questo è un grafo delle relazioni, e una reale espansione di ciò che questo servizio sa, e nel contesto per cui la funzionalità è stata creata (un paziente e il suo dietista), un arco in quel grafo è di per sé un dato personale legato alla salute, perché indica che qualcuno è sotto cure mediche. È il minimo necessario per autorizzare la lettura; entrambe le parti acconsentono, poiché chi concede crea la riga e chi riceve può eliminare la propria parte; l'arco viene eliminato definitivamente alla revoca e scompare a cascata quando uno dei due account viene eliminato. Un'installazione che non impostaSYNC_SHARINGnon memorizza alcun grafo simile e non ha alcuna tabella in cui inserirlo.
- Stime segnalate, su una distribuzione con
SYNC_FEEDBACKimpostato (§5.25, ADR-0006): le cifre di ogni voce che una persona ha scelto di segnalare, la foto del piatto quando il dispositivo ne aveva ancora una, l'id dell'account, la registrazione del consenso e l'orario di arrivo, tutti leggibili. Vengono conservati perinstance.feedback.retentionDays, poi eliminati da una pulizia, e vengono rimossi insieme all'account. Un gestore può leggerli tramite il §5.20, e ogni lettura di una fotografia viene registrata. Una distribuzione senza il flag non ha alcuna segnalazione e alcuna fotografia da conservare.
Non ricavabile dai metadati sopra indicati: cosa è stato mangiato, quando, quanto o qualsiasi altra cosa all'interno del payload. Due voci sopra riportate lo rivelano. Il codice di recupero sigillato apre l'intero diario a chiunque detenga anche SERVER_SECRET, e una stima segnalata mostra la singola voce che contiene.
Istanze gestite
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.
Documentazione: protocollo
Rilasci: note di rilascio