An open-source, self-hosted food tracker with BYOK (bring-your-own-key) AI plate identification. Snap a photo of your plate and your own AI provider (OpenRouter, Mistral, any OpenAI-compatible endpoint, or Anthropic) estimates the macros. Your key, your provider, your data.
There are no accounts. No sign-up, no login, no password: open the app and start logging. Your diary lives in your browser's own IndexedDB on the device you use, and the app server has no database at all: one stateless container, no secrets, nothing to provision. Optional end-to-end-encrypted sync between devices is a separate service you can ignore forever.
https://openplate.lowcarbcheck.org runs this code. There is nothing to sign up for: open it and start logging, the same as a local install.
It is a demo instance, so treat it as one: no uptime promise, no support, and nothing there is backed up for you. Your diary lives in that browser's storage, and clearing the browser clears it. Self-hosting is the equally supported option, and it is the one below.
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/compose.yml docker compose -f compose.yml up -d
That is the whole setup: one container, no database, no .env step, no secret to generate.
The app is reachable at http://localhost:3000.
Upgrading is docker compose -f compose.yml pull && docker compose -f compose.yml up -d. There is no server-side state to
migrate or lose.
openplate ships English and German. A visitor who has not chosen yet sees
English; set DEFAULT_UI_LANGUAGE=de to start them in German instead.
DEFAULT_UI_LANGUAGE=de
It is a starting language, not a lock: whatever you set, anyone can switch in Settings and their choice sticks. An unsupported code fails the boot rather than falling back, so a typo is loud instead of serving the wrong language forever.
This is the interface only. It does not translate food names or AI replies.
If you also want end-to-end-encrypted sync across devices,
docker/topologies/compose.sync.yml brings up the app, the
openplate-sync service, and the Postgres that
sync (and only sync) needs:
curl -O https://raw.githubusercontent.com/LowCarbCheck/openplate/main/docker/topologies/compose.sync.yml echo "SERVER_SECRET=$(openssl rand -hex 32)" >> .env # The URLs a BROWSER will use. Skip these two only for a localhost trial. echo "PUBLIC_APP_URL=https://openplate.example.com" >> .env echo "PUBLIC_SYNC_URL=https://sync.example.com" >> .env docker compose -f compose.sync.yml up -d
There are two larger shapes as well: self-hosted AI, and everything at once.
docker/topologies/README.md is the one-page map of all four,
and compose.full.yml in particular needs four values edited inside the file before it will
work.
Full walkthrough: docs/self-hosting.md and docs/sync.md.
- Open the app and go through the short onboarding: there is nothing to sign up for.
- Go to Settings → AI and connect an AI provider with your own API key. The fastest path is Connect with OpenRouter (one-click OAuth, no key to copy-paste). The key is stored only in that browser and is sent to nobody but the provider you chose.
- Use Scan Plate to upload a photo and get an AI-estimated macro breakdown, or Add to log entries manually.
- Profile → Your data downloads a full JSON backup and imports one. Your diary lives on this device, so that file is how you move it and how you keep it safe. Plate photos stay on the device that took them: they are never exported or synced.
Run any subset. Only the first one is required.
| Component | What it is | Needed? |
|---|---|---|
| openplate (this repo) | The app. Accountless, local-first, stateless, boots with no secrets. | Yes, it is the product. |
| openplate-sync | An account service whose first feature is end-to-end-encrypted sync. Stores an email address and ciphertext it holds no key for. It also backs the optional research console at /study (docs/sync.md), which stays dark unless the sync service sets SYNC_RESEARCH=true (off by default). |
No. Everything works without it. |
| openplate-inference | A self-hosted, OpenAI-compatible plate-photo endpoint: open-weight models, your own hardware. | No. BYOK cloud providers work without it. |
| Archived 2026年09月04日 (M192), merged into openplate-sync: a managed instance's own account now carries the AI allowance, so the separate proxy is gone. | n/a |
| Guide | What it covers |
|---|---|
| Architecture | The four programs, what each one stores, and how they compose |
| Self-hosting | Compose walkthroughs, first run, HTTPS, backups, upgrading |
| Configuration | Every environment variable, the Content-Security-Policy, custom and instance-provided AI endpoints |
| Sync | Enabling sync across devices, the encryption, and the operator's escrowed recovery key |
| Topologies | What to run, from a browser-only install up to a self-hosted household |
| Family setup | Sharing one AI bill across a household, with a spend limit and revocation per person |
| Legal review | Status of the German legal text, machine-translated and awaiting a lawyer |
Repository-level specifications live at the root: .adr/ (architecture decision
records). Start with
ADR-0006 for why this server has no
accounts. Also AGENTS.md: coding guidelines for this repo;
CLAUDE.md imports it. CONTRIBUTING.md: how to send a
pull request. SECURITY.md: reporting a vulnerability.
Requires Node.js ≥ 22 and pnpm, on Linux x64 or arm64. There is no database to install and no service to start alongside it.
The repo pins an exact pnpm in package.json's packageManager field, so run corepack enable first and let it fetch that version: a different global pnpm installs against a
lockfile it does not match. pnpm-workspace.yaml also restricts optional native packages to
Linux, so an install on macOS or Windows silently resolves none of them; build in a container
there.
corepack enable git clone https://github.com/LowCarbCheck/openplate.git cd openplate pnpm install pnpm dev # http://localhost:3000, nothing to provision first
pnpm typecheck # react-router typegen && tsc pnpm lint # oxlint --max-warnings 0 pnpm test:unit # node --test against tests/unit/** pnpm test:integration # node --test against tests/integration/** pnpm build # react-router build (NODE_ENV=production) pnpm start # tsx ./server.ts (NODE_ENV=production)
Built with React Router v8, Express, and IndexedDB in the browser. Every environment variable
is optional tuning: copy .env.example to .env if you want to change one. The image is
built from Dockerfile.pnpm; its entrypoint just starts the server, with no migration or
provisioning step.
openplate is open source under the MIT License (SPDX: MIT), as is
openplate-sync. Run it, read it, change it,
fork it, redistribute it, host it for others, commercially or not, with no restrictions
beyond keeping the copyright and license notice attached to any copy you distribute.