The app
openplate logs food and, if you want it, estimates the macros on a plate from a photo. Search a food or scan a plate, and the entry lands in your diary for the day. On your own install, there is no sign-up screen: open the app and start logging. On the hosted instance, you sign in first.
The diary is one day at a time: the meals you logged, what they cost against the day's limit, and what is left of it.
These screens show example data, not a real diary.

No app store in between
openplate is a web app (a PWA) that you install from the browser, on a phone or a computer. There is no app store. Nobody needs an Apple or Google account, and no store review or store fee stands between a release and your device. An institution that hosts openplate serves it from its own domain, and an update reaches every device the next time the app opens.
Reminders are optional. If you turn them on, they travel through the push service of your browser's maker, as every web push does.
What it stores on the device
Everything a user owns (food logs, weights, personal foods, goals, the AI settings) is written to the browser's IndexedDB on the device it was entered on (app/lib/local-store/). It is stored in the clear there, because it is your device, and it never leaves it except in two forms you choose: a JSON export you download, or an encrypted sync blob.
The app server is a single stateless container. No database, no ORM, no migrations, and no secret it needs in order to boot. It can hold exactly one optional secret, the operator's key for the food database, described below. Destroying the container loses nothing. That is not thrift, it is the whole promise: see ADR-0006.
What leaves the device
A plate photo is read in the browser and posted straight to whichever OpenAI-compatible endpoint you configured. The openplate server is never in that request. It is not uploaded here, not written to disk, not logged. Only the resulting numbers are saved, into the device's local store; the photo stays on the device that took it, excluded from JSON exports and from sync payloads alike.
That endpoint is either a cloud provider you pay (the BYOK path) or your own openplate-inference container. In the self-hosted case the model names the foods on the plate and estimates grams, and then the macros are looked up, not invented: carbs, protein, fat and kcal are resolved by name against the configured food source, by default a bundled extract of USDA FoodData Central (8,041 generic foods shipped inside the image, no network call, public domain). The language model never authors a macro number.
Because the browser makes that call, the endpoint must be an address a browser can reach. A compose hostname like http://inference:8300/v1 will not work even though the two containers can reach each other that way. Use the host's LAN address, a tailnet name, or a hostname on your reverse proxy.
Docs: architecture, self-hosting