Saltar al contenido
openplate

La aplicación

Arquitectura

Los tres programas y la base de datos de alimentos, qué contiene cada uno y cómo se componen

Esta página es una traducción automática de la documentación en inglés.

Tres programas, un producto y dos complementos opcionales, más un servicio externo que responde nombres de alimentos. Esta página explica qué guarda cada uno y cuál se interpone en la ruta de tus datos.

El diagrama siguiente muestra todo el sistema en cinco flechas. Tu dispositivo guarda el diario y la foto del plato. El diario sale cifrado hacia openplate-core. La foto sale hacia el endpoint de IA que hayas configurado. El servidor de la app envía la página y pasa los nombres de los alimentos que buscas a una base de datos de alimentos. No se interpone en la ruta del diario ni en la de las fotos.

El dispositivo guarda el diario y la foto. El diario sale cifrado hacia openplate-core, la foto va al endpoint de IA que hayas configurado y los nombres de los alimentos pasan por el servidor de la app hacia la base de datos de alimentos.
Código fuente del diagrama
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"]

Detrás de "tu endpoint de IA" caben tres cosas: un proveedor en la nube donde tengas una clave, una máquina con openplate-inference en tu propio hardware o, en una instancia administrada, el propio servidor central, que reenvía la foto y la descuenta de tu cuota. topologies.md ilustra las cuatro formas de ejecutar openplate, con una pequeña imagen para cada una.

El cliente es el producto

Todo lo que un usuario posee (registros de comida, pesos, alimentos personales, objetivos y la configuración de IA) se escribe en el IndexedDB del navegador dentro del dispositivo donde se introdujo (app/lib/local-store/). Se almacena allí en claro, porque es tu dispositivo, y nunca sale de él salvo en dos formas que tú eliges: una exportación en JSON que descargas o un blob de sincronización cifrado.

El servidor de la app es un único contenedor sin estado. Sin base de datos, sin ORM, sin migraciones y sin ningún secreto necesario para arrancar. Puede contener exactamente un secreto opcional, la clave del operador para la base de datos de alimentos, descrita más abajo. Destruir el contenedor no hace perder nada. No es tacañería, es toda la promesa: consulta ADR-0006.

La sincronización es identidad, al margen de la ruta de la foto y nunca dentro de ella

openplate-core mueve un diario entre dispositivos. Es el único servicio en openplate que almacena cuentas. Es un desplegable independiente con su propia imagen, base de datos y secreto, y el navegador habla con él directamente. El servidor de aplicaciones no actúa como proxy en su nombre ni sirve ninguna ruta de sincronización.

El diario se cifra antes de salir del dispositivo. El cliente serializa el almacén local, lo comprime con gzip, lo cifra con AES-256-GCM bajo una clave de datos aleatoria y sube el resultado como un único blob opaco. La clave de datos se envuelve bajo una clave derivada de tu frase de contraseña: la frase de contraseña se extiende con Argon2id y se divide mediante HKDF en ramas independientes. Dos de ellas permanecen en el dispositivo y desenvuelven lo que es tuyo, y una tercera se envía como credencial de inicio de sesión. Son hermanas, no padre e hija, por lo que poseer la credencial no revela nada sobre la clave.

El operador guarda una clave de recuperación. Al registrarte, la app genera un código de recuperación y envuelve la clave de datos bajo él. Envía el código a openplate-core, que lo sella bajo su propio secreto. Eso es lo que permite que un restablecimiento de contraseña por correo devuelva tu diario en lugar de una cuenta vacía. También significa que el operador de una instancia puede restaurar, y en principio leer, un diario alojado en ella. En una instancia que hospedes tú, tú eres ese operador. sync.md expone el intercambio al completo.

Lo que el servidor ve además del texto cifrado se detalla con claridad en PROTOCOL.md §9: una dirección de correo, el tamaño del blob, la frecuencia y hora de las escrituras, números de versión y parámetros KDF.

La consola de estudio opcional mantiene sus propias cuentas separadas en el mismo servidor. Sincronización indica cuándo está activada, y ADR-0008 explica por qué reside en la aplicación.

La inferencia es cómputo, y la foto va a ella directamente

Una foto del plato se lee en el navegador y se envía directamente al endpoint compatible con OpenAI que hayas configurado. El servidor de openplate nunca interviene en esa petición. No se sube aquí, no se escribe en disco, no se registra. Solo se guardan los números resultantes en el almacén local del dispositivo; la foto se queda en el dispositivo que la tomó, excluida tanto de las exportaciones en JSON como de las cargas útiles de sincronización.

Ese endpoint es un proveedor en la nube que pagas (la opción BYOK) o tu propio contenedor openplate-inference. En la opción autohospedada, el modelo nombra los alimentos del plato y estima los gramos, y después los macronutrientes se buscan, no se inventan: los carbohidratos, las proteínas, las grasas y las kcal se resuelven por nombre contra la fuente de alimentos configurada, por defecto un extracto integrado de USDA FoodData Central (8.041 alimentos genéricos incluidos dentro de la imagen, sin llamadas de red, de dominio público). El modelo de lenguaje nunca genera un valor de macronutrientes.

Como el navegador realiza esa llamada, el endpoint debe ser una dirección que un navegador pueda alcanzar. Un nombre de host de compose como http://inference:8300/v1 no funcionará, aunque los dos contenedores puedan comunicarse entre sí de esa forma. Usa la dirección LAN del host, un nombre de tailnet o un nombre de host en tu proxy inverso.

La base de datos de alimentos es una búsqueda por nombre, a través del servidor de la app

Un modelo en la nube o gestionado devuelve su propia estimación de macros para cada alimento que encuentra. Luego, la app comprueba esos alimentos con una base de datos curada. El navegador envía únicamente los nombres que el modelo encontró a /api/food-matches del servidor de la app. El servidor busca cada nombre en LowCarbCheck (FOOD_DB_API_URL). Una coincidencia puede sustituir la estimación del modelo en la pantalla de confirmación. La búsqueda en la pantalla Añadir y los valores de referencia en la pantalla Nutrientes llegan a través de la misma búsqueda en el servidor.

Esta es la única ruta en la que se interpone el servidor de la app, y es estrecha por diseño:

  • Transporta nombres de alimentos, nombres de nutrientes y el idioma de la pantalla. Nunca una foto, nunca tu clave de IA, nunca una entrada del diario.
  • LowCarbCheck ve la dirección del servidor de la app y la clave de la instancia, nunca tu dirección. Cada persona en una instancia comparte esa clave y su cuota.
  • El servidor almacena las respuestas en caché, y solo una búsqueda que no esté en caché cuenta para el límite de peticiones por dirección.
  • Falla en modo permisivo. Si la base de datos es inaccesible, rechaza la clave o agota la cuota, el escaneo se completa igualmente con los propios números del modelo, y la pantalla así lo indica.
  • FOOD_DB_API_URL="" lo desactiva, y entonces ningún nombre de alimento sale de tu servidor.
  • Con FOOD_DB_BACKFILL=true también incluye propuestas: los nombres de un alimento que una persona guardó a partir de una respuesta de IA, en todos los idiomas de la aplicación, y los macros por 100 g si el alimento no tiene coincidencias. Nunca incluye un nombre que la persona haya escrito, ni una foto, ni una entrada del diario. Consulta configuration.md.

Se ejecuta en el servidor en lugar de en el navegador. Esto mantiene la clave fuera de la página y permite que la configuración del operador decida si los nombres salen o no. La clave es FOOD_DB_API_KEY. Sin ella, la instancia usa el nivel anónimo de LowCarbCheck; configuration.md lista los niveles.

El servidor central gestiona las cuentas y se sitúa delante del cómputo en una instancia administrada

Una instancia puede definir INSTANCE_MODE=managed (consulta configuration.md). Eso declara una sola cosa: una organización opera esta instancia, invita a sus miembros por correo electrónico y asigna a cada uno una cuota diaria de IA. openplate-core gestiona eso; la cuenta que ya almacena para sincronización también almacena la cuota, por lo que no hay un segundo paso de conexión ni una segunda credencial.

Para el navegador nada cambia: una cuenta iniciada con cuota escanea mediante el proxy de IA que expone openplate-core, el mismo servicio con el que el cliente ya se comunica para sincronizar. Para lo que hay detrás, openplate-core actúa como cliente: apunta a un proveedor en la nube o a tu propio contenedor de openplate-inference. La inferencia es la capa de cómputo, openplate-core es la capa multiinquilino en una instancia gestionada, y ambas se componen: el servidor central no aloja ningún modelo ni responde a ningún escaneo por sí mismo.

Está en la ruta de las fotos, lo cual es el coste real del enfoque, y la mitigación es una propiedad del código más que un ajuste: el tipo de campo del registrador solo admite tipos primitivos, de modo que ningún cuerpo puede llegar a una línea de registro, y las cadenas de error del proveedor upstream se depuran antes de registrarlas o devolverlas. Los miembros de una organización comparten gastos, no datos; la foto del plato que llega al proxy se lee una sola vez y no se almacena.

La cuota cuenta peticiones, no dinero. Un operador también puede fijar un límite diario para toda la instancia (AI_INSTANCE_DAILY_LIMIT), y sigue siendo necesario un límite de gasto en la clave principal ante el proveedor.

Un administrador gestiona la instancia desde /admin en la app: las personas y sus cuotas, invitaciones, actividad, estimaciones reportadas y qué valores de referencia cita la pantalla Nutrientes.

Historial

De agosto a septiembre de 2026, esto era un servicio independiente, openplate-gateway: un proxy pequeño compatible con OpenAI que contenía una clave upstream y emitía a cada miembro un token opk_… con su propio límite diario. La versión M192 (septiembre de 2026) lo integró en openplate-core: una misma cuenta contiene ahora tanto el diario como la cuota, por lo que no hay un segundo servicio, ni un segundo enlace de invitación, ni una segunda credencial que repartir.

Qué más puede transportar openplate-core

Cada función siguiente está desactivada por defecto, y cada una cambia lo que guarda el servidor. El README de openplate-core describe cada una de ellas.

  • Compartir un diario con un médico (SYNC_SHARING=true). El propietario envuelve la clave de datos una tercera vez, bajo la clave pública del médico, y el servidor almacena esa clave envuelta. El navegador del médico la desenvuelve y lee el diario en /shared. La acción de compartir no le da al servidor nada nuevo que abrir.
  • Aportaciones a investigaciones (SYNC_RESEARCH=true). Una persona se inscribe en un estudio desde un enlace y envía totales diarios bajo un seudónimo. Los totales se sellan para el estudio, pero el servidor sabe qué cuenta contribuye a cada estudio.
  • Estimaciones notificadas (SYNC_FEEDBACK=true). «Notificar una estimación incorrecta» envía la foto, las cifras y un registro de consentimiento al servidor, donde un administrador los revisa en /admin. A diferencia de un escaneo, esa foto sí se almacena.
  • El pulso no requiere configuración por parte del operador: cada persona lo activa en Ajustes, Compartir. Envía recuentos redondeados. Una comida cuenta una vez, con sus calorías redondeadas a 50 y sus proteínas a 5 g. Un escaneo cuenta una vez, y un ayuno en curso envía un pulso. La pantalla principal muestra el total de la instancia para hoy. El servidor conserva los totales del día y un registro de quién contribuyó cada día, durante 30 días.
  • Notificaciones push (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT). El servidor almacena una suscripción por dispositivo: la dirección push del navegador y sus claves, una zona horaria, un idioma y los ajustes de los recordatorios. Envía a cada dispositivo como máximo dos notificaciones al día, y cada una contiene solo su tipo, nunca texto sobre ti. El servicio push de tu navegador se encarga de entregarla.
  • Planes de pago (PLANS_UPSTREAM_URL, PLANS_UPSTREAM_SECRET). openplate-core reenvía /v1/plans/* a un servicio de planes que gestiona el operador, junto con el identificador de la cuenta y la dirección de correo electrónico. La app muestra Ajustes, Plan únicamente cuando el servidor indica que los planes están activados.

BYOK es la opción sin servidores

Sin ningún contenedor de inferencia, el navegador llama directamente a un proveedor en la nube con una clave que introduces en ese dispositivo. La clave se almacena en el almacenamiento local del dispositivo, se excluye de la exportación JSON y nunca se envía al servidor de openplate: no existe una copia en el servidor, cifrada o no, ni un proxy de la llamada en el servidor.

La Content-Security-Policy de producción forma parte de esa promesa y no es mero adorno: la lista de permitidos connect-src se deriva del registro de proveedores, y es lo que impide que un script inyectado robe una clave que reside en la página. Consulta configuration.md.

Quién conserva qué

ComponenteQué almacenaQué ve en tránsito
Tu navegadorEl diario completo, en claro, en IndexedDB. Tu clave de IA. Fotos del plato en caché.Todo. Es tu dispositivo.
servidor de la app de openplateSin base de datos, sin cuentas, sin diario. Como máximo un secreto: la clave del operador para la base de datos de alimentos.Peticiones de página y los nombres de los alimentos que buscas o escaneas, que reenvía a la base de datos de alimentos. Nunca una foto, nunca tu clave de IA, nunca una entrada del diario, nunca un blob de sincronización.
openplate-core (opcional)Una dirección de correo electrónico, un verificador de autenticación, parámetros KDF, el diario como texto cifrado y el código de recuperación en custodia que permite descifrarlo. En una instancia administrada, también la cuota diaria de cada cuenta y el contador de uso. Con las funciones anteriores activadas, también lo que lista cada una de ellas.Tamaño del blob, tiempos de escritura, metadatos de sesión. En una instancia administrada, también la foto reenviada al proxy de IA, únicamente durante el tiempo que tarda en reenviarla, leída una sola vez, sin almacenar.
openplate-inference (opcional)Nada por usuario: ni cuentas, ni sesiones, ni cookies. Los pesos del modelo y un conjunto de datos de alimentos.La foto que le enviaste, únicamente mientras dura la petición. Con la fuente de alimentos predeterminada no hace ninguna llamada saliente, salvo la descarga inicial de los pesos. Con FOOD_SOURCE=lcc o off envía fuera nombres de alimentos, nunca la foto.
Base de datos de alimentos de LowCarbCheck (activado salvo que se desactive)Un contador de uso por clave, o por dirección de red para quien haga peticiones sin ella.Nombres de alimentos y un idioma, procedentes del servidor de la app, con la clave de la instancia. Nunca una foto, y nunca quién eres.
Proveedor de IA en la nube (vía BYOK)Lo que indique su política.La foto y tu clave. Se aplican sus condiciones, no las nuestras.

Edita esta página en GitHub