Skip to content
openplate

The app

Environment variables

Every variable the app, the core server and the inference service read, with its default, and the settings that stop the boot

Every setting of the three openplate containers is an environment variable. This page lists all of them, for the app, the core server (openplate-core) and the inference service. Most are optional. When you leave one unset, the default in its row applies.

Some settings stop the boot on purpose. A value the service cannot use, or one half of a pair, makes the container exit. The exit message names the variable. The container does not start with a guess. Settings that stop the boot lists every such rule.

How to set a variable

  • Docker Compose. Put the line in the .env file next to the compose file, for example LOG_LEVEL=debug. Then run docker compose -f <your file> up -d again. Every shipped compose file passes each variable its service reads on to the container. docker compose restart does not read .env again.
  • Quadlet. Put the line in the <unit>.env file next to the unit, for example app.env, core.env or inference.env. Use the container's own names from this page. Then restart the unit, for example systemctl --user restart core.service. podman.md explains the files.
  • Without a container. The app and the core server each read a .env file in the folder they run in. You can also set the variable in the shell or in the systemd unit. Without Docker shows the app's setup.

The Default column says what the service does when the variable is unset. A compose file can pass a value of its own, for example MODEL_PROFILE: lite. The compose file shows that value next to the name.

Names the compose files fill for you

The three topology files, compose.core.yml, compose.inference.yml and compose.full.yml, fill some container variables from shared names in .env. Set the shared name there. The container variable on its own has no effect in these files.

In .envFills
PUBLIC_APP_URLthe app's APP_URL, and the core server's CLIENT_BASE_URL
PUBLIC_SYNC_URLthe app's CORE_URL, and the core server's SERVER_PUBLIC_URL
PUBLIC_INFERENCE_URLthe app's DEFAULT_INFERENCE_BASE_URL
INFERENCE_API_KEYthe app's DEFAULT_INFERENCE_API_KEY, and the inference service's API_KEYS
POSTGRES_USER, POSTGRES_PASSWORD, SYNC_DB_NAMEthe database, and the core server's DATABASE_URL

docker/compose.yml, the app on its own, takes APP_URL under its own name. The core server's own quickstart file is apps/core/docker/compose.yml. It takes SERVER_PUBLIC_URL and CLIENT_BASE_URL under their own names. It builds DATABASE_URL from POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB.

The app

The app container, ghcr.io/lowcarbcheck/openplate. It starts with nothing set. configuration.md explains its larger features in depth.

Server and addresses

VariableDefaultWhat it doesMore
NODE_ENVproduction in the image, otherwise developmentproduction serves the built app, makes APP_URL required, and makes 1 the TRUST_PROXY default. The image sets it.
APP_URLhttp://localhost:3000, required in productionThe public address people open, for example https://openplate.example.com. The front page puts it in its share links. The server does not start in production without it.Caddy
PORT3000The port the server listens on.
HOSTunset, every interfaceThe address the server listens on. Leave it unset in a container. Without a container, 127.0.0.1 keeps the server on this machine, for a proxy on the same box.Without Docker
TRUST_PROXY1 in production, otherwise offHow many reverse proxies stand in front of the app: a number, true, false, or an Express preset or address range such as loopback or 10.0.0.0/8. The check that stops cross-site form posts needs the right value behind a proxy. Use 0 with no proxy.The app on its own
CSP_CONNECT_EXTRAunsetExtra origins for the connect-src of the Content-Security-Policy, separated by spaces. Your own AI endpoint on another host needs this.Custom AI endpoints

Language and content

VariableDefaultWhat it doesMore
DEFAULT_UI_LANGUAGEenThe language a visitor sees before choosing one: en, de, fr, it, es or tr. A person's own choice always wins. Any other value stops the boot.
NUTRIENT_REFERENCE_BASISdgeThe reference values the Nutrients screen quotes: dge (German DGE), efsa (EU) or us (NASEM). Any other value stops the boot.
CONTENT_DIRunset, no legal pagesA folder of markdown files for the legal pages, mounted read-only. A value that names no folder stops the boot.Content pages

Sync and the kind of instance

VariableDefaultWhat it doesMore
CORE_URLunset, sync offThe address of your core server, as a browser reaches it. Its origin goes into the Content-Security-Policy. On a managed instance the app server also reaches it, to check the account of each food lookup. A malformed value stops the boot.Sync
SYNC_SERVER_URLunsetDeprecated. The old name of CORE_URL. It still works for one more release, and the boot logs one warning when it is the only one set. If both are set to different addresses, the old name wins for this release and the boot logs one warning that names both. Remove the old line before the release that drops the old name.Sync
INSTANCE_MODEopenopen or managed. On a managed instance an administrator invites people, and the core server supplies the AI. managed needs CORE_URL. Any other value stops the boot.Managed instances

Instance-provided AI

VariableDefaultWhat it doesMore
DEFAULT_INFERENCE_BASE_URLunsetAn OpenAI-compatible endpoint this instance offers to every visitor, as a browser reaches it. A malformed value stops the boot.Instance-provided AI
DEFAULT_INFERENCE_API_KEYunsetThe key for that endpoint. It is public: every visitor's browser receives it.Instance-provided AI
DEFAULT_INFERENCE_MODELopenplate-plate-1The model name sent to that endpoint.Instance-provided AI

Food database

VariableDefaultWhat it doesMore
FOOD_DB_API_URLhttps://lowcarbcheck.orgThe LowCarbCheck food database the server looks food names up in. An empty value turns the lookup off.The food database key
FOOD_DB_API_KEYunset, the anonymous tierYour LowCarbCheck key. Only the server reads it, and it never reaches a browser.The food database key
FOOD_DB_BACKFILLfalsetrue passes the foods people save from an AI answer on to LowCarbCheck as proposals. It needs FOOD_DB_API_KEY and stays off without it. Any value other than true or false stops the boot.Proposals to the food database
FOOD_DB_DAILY_CALL_LIMIT3200The most LowCarbCheck calls this server makes in one UTC day. Past it, food lookups pause until midnight UTC and the app says so. The default keeps a free key's 100,000 a month. Anything but a positive whole number stops the boot.The food database key

Analytics, newsletter and updates

VariableDefaultWhat it doesMore
MATOMO_URLunset, analytics offA Matomo install you run yourself. Set it together with MATOMO_SITE_ID.Analytics
MATOMO_SITE_IDunsetThe Matomo site id, a positive whole number. Set it together with MATOMO_URL.Analytics
MATOMO_EVENT_LEVELproductHow much the instance counts: pageviews, product or research. It needs the two above.What a level decides
NEWSLETTER_SUBSCRIBE_URLunset, no formWhere the server forwards the landing page's newsletter form. Set it together with NEWSLETTER_TURNSTILE_SITE_KEY.Newsletter sign-up
NEWSLETTER_TURNSTILE_SITE_KEYunsetThe Cloudflare Turnstile site key of that form. It is not the sign-up captcha, see Sign-up with Turnstile.Newsletter sign-up
UPDATE_CHECKonSetting this to off or false stops the server from fetching openplate.de/latest.json for new releases. It also stops the project's daily count of asks.The release check

Logging

VariableDefaultWhat it doesMore
LOG_LEVELinfoHow much the server logs, as a pino level: debug, info, warn or error.

Closing an instance

VariableDefaultWhat it doesMore
MOVED_TO_URLunsetCloses this instance and directs users to another one, for example https://app.openplate.de. Every page request serves a page that names the new address, the service worker installed on phones clears its caches and unregisters itself, and the API returns 410. The value must be an https:// address on a host other than APP_URL; anything else stops the boot.Moving people to another instance

The core server (openplate-core)

The core server container, ghcr.io/lowcarbcheck/openplate-core. It needs two values: DATABASE_URL, which the compose files fill for you, and SERVER_SECRET. Everything else is optional and off until you set it. openplate-core's README explains the features.

Server and addresses

VariableDefaultWhat it doesMore
NODE_ENVproduction in the imageWith mail set, production requires both link addresses below to be https:// addresses on another host.Mail needs the public addresses
PORT3000The port the service listens on.
HOSTunset, every interfaceThe address the service listens on. Leave it unset in a container. 127.0.0.1 keeps a development instance on its own machine.
TRUST_PROXYfalseHow many reverse proxies stand in front: a number, true or false. With the wrong value behind a proxy, every request seems to come from the proxy. One person can then use up the limit for everybody.Three settings that matter
SERVER_PUBLIC_URLunsetThis service's own public address. It goes into the links in invitation and password reset letters. Set it together with CLIENT_BASE_URL.Mail needs the public addresses
CLIENT_BASE_URLunsetThe address of the openplate app, the other half of those links.Mail needs the public addresses
INSTANCE_NAMEopenplateSets the instance name for the /health handshake (instance.name) and the start-up log. The letters do not use it. Maximum 64 characters.
INSTANCE_LANGUAGEenSets the letter language when a request specifies none. Accepted values are en, de, fr, it, es or tr. Any other value stops the boot.
NUTRIENT_REFERENCE_BASISdgeA new instance starts with one of these reference values: dge, efsa or us. An administrator can change the live setting later through the admin API. Any other value stops the boot.
CONTENT_DIRunsetA folder, mounted read-only, with the text of the declaration letters. The app can read its legal pages from this folder. The service never checks it at boot.Declaration letters
LEGAL_DECLARATION_RECEIPTS_PER_DAY200The most declaration receipts the instance mails in any 24 hours, to all addresses together. After that, a declaration is still recorded and sent to you, but its receipt is skipped.Declaration letters
LEGAL_DECLARATION_RECEIPTS_PER_NETWORK_PER_DAY10The same limit for one sender network, an IPv4 address or an IPv6 /64. A restart resets this count.Declaration letters

Database

VariableDefaultWhat it doesMore
DATABASE_URLnone, requiredThe Postgres connection string. The compose files build it for you.
DATABASE_SSLfalseSet to true if Postgres requires TLS. Accepts true, false, 1 or 0.
MIGRATIONS_DIRdrizzle/migrationsThis sets where the service finds its database migrations at start. The image keeps them there, so leave it unset.

Secrets

VariableDefaultWhat it doesMore
SERVER_SECRETnone, requiredThe root secret must be at least 32 characters. Generate it with openssl rand -hex 32 and back it up with the database. Changing this secret locks out every account permanently.Three settings that matter
ADMIN_TOKENunsetThe operator admin credential must be at least 24 characters. You need it to create the first account. With no token and no administrator account, the admin API returns 404.Create the first account

Accounts and sign-up

VariableDefaultWhat it doesMore
OPEN_SIGNUPunset, invitations onlytrue lets anyone request an account with their own address. This requires mail. The server accepts only true. Any other value, including false, stops the boot.Sign-up with Turnstile
TURNSTILE_SECRET_KEYunset, no captchaThe Cloudflare Turnstile secret key that verifies the sign-up captcha. Set it together with TURNSTILE_SITE_KEY, and only with OPEN_SIGNUP=true.Sign-up with Turnstile
TURNSTILE_SITE_KEYunsetThe public Turnstile site key. /health publishes it, and the app renders the captcha with it.Sign-up with Turnstile

Invitations and trials

VariableDefaultWhat it doesMore
MEMBER_INVITE_DAILY_AI_LIMITunset, members cannot inviteThe number of AI requests per UTC day an account gets when a member invited it, 1 to 10000. Set it together with MEMBER_INVITE_ALLOWANCE_DAYS.Member invites
MEMBER_INVITE_ALLOWANCE_DAYSunsetThe number of days after sign-up that this allowance lasts.Member invites
MEMBER_INVITE_LIFETIME_CAP5The total invitations one member can send, ever: 0 or more. This requires the pair above, or MEMBER_INVITE_TRIAL=true.Member invites
MEMBER_INVITE_TRIALfalsetrue makes a member invitation grant the scan trial below instead of the day allowance. It requires the trial, and cannot be used with the pair above.
TRIAL_SCANSunset, no trialThe free AI scans a new account gets, 1 to 100. Set it together with TRIAL_DAILY_AI_LIMIT, and set TRIAL_ADDRESS_PEPPER with them.
TRIAL_DAILY_AI_LIMITunsetThe number of AI requests per UTC day during the trial, 1 to 10000.
TRIAL_DAYSunset, no end dateThe trial also ends at midnight after this many days, 1 to 90, whichever comes first. It requires the trial pair.
TRIAL_TIME_ZONEUTCThe time zone of that midnight, as an IANA name such as Europe/Berlin. It requires TRIAL_DAYS. An unknown zone stops the boot.
TRIAL_ADDRESS_PEPPERunsetThe secret that enforces one trial per mailbox. It must be at least 32 characters. Required with the trial pair. Changing this value forgets which mailboxes had a trial.
TRIAL_HASH_RETENTION_DAYS365The days a deleted account's mailbox hash is kept, 1 to 3650, counted from the deletion. After that an hourly sweep deletes it, and the same mailbox can have a trial again. An instance with no scan trial keeps no hash.
AI_TRIAL_INSTANCE_DAILY_LIMITunset, no limitThe total amount all trial accounts together can spend per UTC day. Requires the trial.
AI_TRIAL_NETWORK_DAILY_LIMITa tenth of AI_TRIAL_INSTANCE_DAILY_LIMIT, at least 1The amount the trial requests from one network (an IPv6 /64, or one IPv4 address) can spend of the trial limit per UTC day. It is off without AI_TRIAL_INSTANCE_DAILY_LIMIT, and must not be above it. People behind one IPv4 carrier NAT share it.
DEFAULT_FREE_DAILY_AI_LIMITunset, offThe AI requests per UTC day every account gets that has no free limit of its own, 0 to 10000. It never ends and has no scan count. A day used up answers 429 with Retry-After. It replaces the scan trial, so setting it together with the trial pair stops the boot. An account that still holds a trial falls under this limit.
DEFAULT_CAPABILITIESunset, no checkWhat an account with no capability list of its own may use: comma separated labels such as scan,recipes, or none for nothing. Unset or empty means the AI proxy checks no feature and every request passes. A request for a feature the account lacks gets 403 capability-required.
CAPABILITY_SCHEMA_MAPunset, emptyComma separated schemaName:label pairs. A request that asks for the structured output schema you list needs that label, whatever its X-Openplate-Feature header says.

Mail

VariableDefaultWhat it doesMore
SMTP_HOSTunsetThe SMTP server name or address, with no scheme, port, or path.SMTP
SMTP_PORT587465 uses TLS from the first byte. Every other port must upgrade with STARTTLS, except a mail catcher on this machine.SMTP
SMTP_USERunsetThe SMTP username. Set it together with SMTP_PASSWORD, or leave both unset for a server with no login.SMTP
SMTP_PASSWORDunsetThe SMTP password.SMTP
SMTP_FROMunsetThe sender, formatted as address or Name <address>. SMTP requires it.SMTP
MAIL_API_URLunsetA Resend-compatible HTTP mail API. Set it together with MAIL_API_KEY, MAIL_API_FROM, and MAIL_OPERATOR_EMAIL.An HTTP mail API
MAIL_API_KEYunsetThe mail API key, sent as a Bearer token.An HTTP mail API
MAIL_API_FROMunsetThe sender address for the mail API.An HTTP mail API
MAIL_OPERATOR_EMAILunsetYour own address. It receives your copy of a cancellation or a withdrawal. Both transports require it.SMTP
NODE_EXTRA_CA_CERTSunsetThe path inside the container to a PEM file with extra certificate authorities. Node.js reads it at start. Mount the file for a mail relay whose certificate a private authority signed.A relay with a private certificate authority

AI proxy and limits

VariableDefaultWhat it doesMore
UPSTREAM_BASE_URLunset, no AIThe provider's OpenAI-compatible address, for example https://openrouter.ai/api/v1. Set it together with UPSTREAM_API_KEY.Managed instances
UPSTREAM_API_KEYunsetThe provider key. It never reaches a browser.Managed instances
UPSTREAM_ZDRunsetOpenRouter only. Set it to true and the proxy asks OpenRouter to route a request only to endpoints with zero data retention. Any other host ignores it. Another value than true, false or empty stops the boot.The AI proxy
UPSTREAM_PROVIDER_ONLYunsetOpenRouter only. A comma separated list of provider slugs, for example google-vertex. A request goes to one of them or fails, and never falls back to another provider. Any other host ignores it.The AI proxy
UPSTREAM_TIMEOUT_MS120000The time in milliseconds the proxy waits for the provider's first answer, and then between parts of it.
AI_TIERS_FILEunsetWhere the model of each kind of request comes from. Unset, the model is AI_ADVERTISED_MODEL, with the routing from UPSTREAM_ZDR and UPSTREAM_PROVIDER_ONLY, exactly as before the file existed. bundled uses the file in the core image, ai-tiers.json, which holds the model, its routing and its price. An absolute path uses a file you mount. A bad value or a bad file stops the boot.The AI proxy
AI_ADVERTISED_MODELunsetThe model every scan uses when there is no tier file. /health names it, and the proxy writes it into every request. The app does not scan without a model. With a tier file it only overrides the model of the default tier, as an emergency measure, and the core logs a warning at every start.Managed instances
AI_MAX_OUTPUT_TOKENS8192The maximum output tokens one request can ask for.
AI_RATE_LIMIT_PER_MINUTE20The maximum requests one account can make in any 60 seconds.
AI_INSTANCE_DAILY_LIMITunset, no limitThe daily instance limit in AI units per UTC day. One plate scan costs one unit.The AI proxy
AI_BUDGET_ALERT_FRACTION0.2On an OpenRouter key, mail MAIL_OPERATOR_EMAIL once per reset period when less than this share of the key's limit is left. Above 0 and below 1.The AI proxy
AI_MAX_REQUEST_BYTES8000000The largest request the proxy accepts, in bytes.
AI_MAX_IMAGE_PARTS1Maximum images per request. Requests exceeding this limit return 400 ai-request-too-large before counting starts.
AI_MAX_TEXT_BYTES49152Maximum text bytes per request, combining message text and response_format. Requests with more return the same 400.
AI_MAX_MESSAGES4Maximum messages per request. Requests exceeding this limit return the same 400.
AI_UNIT_INPUT_TOKENS8192Estimated input tokens per AI unit. Each request costs one unit, plus one per additional AI_UNIT_INPUT_TOKENS. Daily limits count these units.The AI proxy
AI_IMAGE_INPUT_TOKENS1500Estimated input tokens for one image. Text counts as its total bytes divided by 4.
VariableDefaultWhat it doesMore
HEALTH_CONSENT_VERSIONunset, no consent askedThe health data consent version every account must accept, such as 2026-09-28. Use 1 to 32 letters, digits, ., _ or -. A changed version asks everybody again.Explicit consent

Push

VariableDefaultWhat it doesMore
VAPID_PUBLIC_KEYunset, no notificationsThe public key for web push. Set all three or none. Make a pair with pnpm core-api push keygen.
VAPID_PRIVATE_KEYunsetThe private key for web push.
VAPID_SUBJECTunsetHow a push service reaches you: a mailto: address or an https:// address.
PUSH_ENDPOINT_HOSTSunsetAdditional comma-separated push hosts a device can register. *.example.org covers every host under example.org. Default browser push services are always permitted. Set this only for custom hosts. A malformed entry stops startup.

Plans

VariableDefaultWhat it doesMore
PLANS_UPSTREAM_URLunset, no plansThe internal address of the billing service that receives /v1/plans/*. Set it together with PLANS_UPSTREAM_SECRET.Paid plans
PLANS_UPSTREAM_SECRETunsetThe shared secret the billing service checks.Paid plans
BILLING_TOKENunsetThe billing service's own credential, at least 24 characters. It reaches three admin routes and nothing else.Paid plans
BILLING_MAX_DAILY_AI_LIMIT1000Maximum daily AI limit that BILLING_TOKEN can write to an account. Keep this at or above your highest plan. ADMIN_TOKEN is not bound by it.Paid plans

Feedback, sharing and research

VariableDefaultWhat it doesMore
SYNC_FEEDBACKfalsetrue accepts reported estimates with their photographs, kept for 30 days. You can view those photographs.Reported estimates
FEEDBACK_DAILY_LIMIT5The reports one account can send per UTC day.
FEEDBACK_MAX_REQUEST_BYTES8000000The largest report the service accepts, in bytes.
SYNC_SHARINGfalsetrue turns on sharing a diary with a clinician.
SYNC_RESEARCHfalsetrue turns on research contributions and the study console.Sync

Logging and other

VariableDefaultWhat it doesMore
LOG_LEVELinfodebug, info, warn or error. Any other value stops the boot.
SYNC_NOTICEunsetA short message every app shows when it connects, at most 280 characters.
SYNC_NOTICE_URLunsetA link next to the notice, https:// or http://. It needs SYNC_NOTICE.
SERVICE_VERSIONunset, the image's versionReplaces the version /health reports. Leave it unset.

The inference service (openplate-inference)

The inference container, ghcr.io/lowcarbcheck/openplate-inference. It runs the model runtime and the service in one container. openplate-inference's configuration guide explains the options in depth.

Service and access

VariableDefaultWhat it doesMore
PORT8300The port the service listens on. It is the only port the container publishes.
API_KEYSunset, a temporary keyThe keys a caller must send, separated by commas. When unset, the service creates one key at start, prints it once, and forgets it at the next restart.Get the key
LOG_LEVELinfodebug, info, warn or error. Any other value stops the boot.
PROFILEset from MODEL_PROFILEThe profile name the start-up log prints: lite, quality or custom. The container sets it from MODEL_PROFILE, and it changes nothing else.

Model and weights

VariableDefaultWhat it doesMore
MODEL_PROFILEliteThe weights the container downloads and runs: lite, lite-apache or quality. external downloads nothing and uses your own runtime at MODEL_RUNTIME_URL.Hardware
MODELS_DIR/modelsWhere the weights live in the container, on a volume. Change it only when you mount the weights elsewhere.
WEIGHTS_MIRROR_BASEunsetA mirror to download the weights from, with Hugging Face as the fallback. The service checks checksums either way.
MODEL_RUNTIME_URLhttp://127.0.0.1:8080, the bundled runtimeThe address of your own runtime, without /v1. MODEL_PROFILE=external requires it. With any other profile, a different address stops the container.External-mode variables
MODEL_RUNTIME_API_KEYunsetA key the service sends to your runtime, for a runtime or proxy that requires one.External-mode variables
MODEL_IDopenplate-plate-1The model name the service sends to the runtime. vLLM needs its exact served name.External-mode variables
RUNTIME_PORT8080The port of the bundled llama-server, on the container's loopback address only.
CONTEXT_SIZE8192The context for each scan in flight. The container multiplies it by CONCURRENCY for llama.cpp.
LLAMA_THREADSthe number of cores minus two, at least 1The CPU threads for llama.cpp.
LLAMA_EXTRA_ARGSunsetExtra flags for the end of the llama-server command, split at spaces. Bundled runtime only.
GPU_LAYERSdetected: 99 with a GPU, otherwise 0How many model layers go to the GPU. 0 forces the CPU.
NVIDIA_VISIBLE_DEVICESset by the NVIDIA container runtime--gpus all sets it. Any value other than void or none makes the container use the GPU. You do not set it yourself.

Limits

VariableDefaultWhat it doesMore
CONCURRENCY2The scans in flight at one time. It also sets llama.cpp's slots.
MAX_QUEUE_DEPTH8The scans that can wait. Past this, a caller gets 429.
RATE_LIMIT_RPM60The requests per minute for each key.
LATENCY_CEILING_MS0, offThe service refuses a scan it cannot finish in this many milliseconds.Hardware
RUNTIME_COMPLETION_TIMEOUT_MS600000The longest one call to the runtime can take, in milliseconds. 0 turns the limit off.
MAX_IMAGE_BYTES8388608The largest photo the service accepts after decoding, in bytes (8 MiB).
IMAGE_MAX_LONG_EDGE896The long edge the service scales each photo down to, in pixels, at least 112.

Food data

VariableDefaultWhat it doesMore
FOOD_SOURCEfdcWhere the macros come from: fdc (the USDA extract in the image, no network), off (Open Food Facts), lcc (LowCarbCheck) or none.Food data
FDC_DATASET_PATH./data/fdc-foods.jsonThe USDA extract, relative to the working directory.
OFF_API_URLhttps://world.openfoodfacts.orgThe Open Food Facts address, read with FOOD_SOURCE=off.
LCC_API_URLhttps://lowcarbcheck.orgThe LowCarbCheck address, read with FOOD_SOURCE=lcc.
LCC_API_KEYunset, the anonymous tierYour LowCarbCheck key, read with FOOD_SOURCE=lcc.
EMBEDDING_RUNTIME_URLunsetAn OpenAI-compatible runtime that serves /v1/embeddings, for better food matching.
EMBEDDING_RUNTIME_API_KEYunsetThe key for that runtime.

Settings that stop the boot

A container that does not start is easy to notice, and it costs one restart. A setting that is quietly ignored lets you believe something works when it does not. So each rule below stops the boot, and the log names the variable.

Refused names

These names were settings once. Now the service refuses to start while one is set, and says what to use instead. The core server refuses them even with an empty value, so delete the line. The app refuses GATEWAY_URL only when it has a value.

VariableRefused byUse instead
GATEWAY_URLthe appINSTANCE_MODE=managed. The core server took over the AI proxy.
SIGNUP_MODEthe core serverNothing. Accounts come from invitations, and OPEN_SIGNUP=true lets people ask for one.
SIGNUPS_OPENthe core serverNothing, for the same reason.
REQUIRE_EMAIL_VERIFICATIONthe core serverNothing. The invitation is the address check.
EMAIL_FROMthe core serverMAIL_API_FROM, or SMTP_FROM.
SMTP_SECUREthe core serverNothing. SMTP_PORT decides the encryption.
PIGEON_API_KEYthe core serverMAIL_API_KEY, or SMTP_USER and SMTP_PASSWORD.
PIGEON_BASE_URLthe core serverMAIL_API_URL, or SMTP_HOST.

Rules between variables

The app.

  • MATOMO_URL and MATOMO_SITE_ID: set both or neither. MATOMO_EVENT_LEVEL needs both.
  • NEWSLETTER_SUBSCRIBE_URL and NEWSLETTER_TURNSTILE_SITE_KEY: set both or neither.
  • INSTANCE_MODE=managed needs CORE_URL.
  • CORE_URL and the deprecated SYNC_SERVER_URL: set one. If both are set to different addresses, SYNC_SERVER_URL wins for this release and the boot logs one warning.
  • APP_URL is required when NODE_ENV=production.
  • MOVED_TO_URL must be an https:// address on a host other than APP_URL, without a user name or password.
  • A value outside its list stops the boot: DEFAULT_UI_LANGUAGE, NUTRIENT_REFERENCE_BASIS, INSTANCE_MODE, MATOMO_EVENT_LEVEL and FOOD_DB_BACKFILL. So does a FOOD_DB_DAILY_CALL_LIMIT that is not a positive whole number. Malformed addresses in CORE_URL, DEFAULT_INFERENCE_BASE_URL, MATOMO_URL or NEWSLETTER_SUBSCRIBE_URL also stop the boot. Boot also stops if MATOMO_SITE_ID is not a positive whole number, or if CONTENT_DIR is not a folder.

The core server.

  • DATABASE_URL and SERVER_SECRET are required.
  • Minimum lengths: 32 characters for SERVER_SECRET and TRIAL_ADDRESS_PEPPER, 24 for ADMIN_TOKEN and BILLING_TOKEN.
  • Mail uses one transport. The HTTP mail API needs MAIL_API_URL, MAIL_API_KEY, MAIL_API_FROM and MAIL_OPERATOR_EMAIL, all four. SMTP needs SMTP_HOST, SMTP_FROM and MAIL_OPERATOR_EMAIL, and SMTP_USER and SMTP_PASSWORD go together. Variables of both transports at once stop the boot, and so does MAIL_OPERATOR_EMAIL with no transport.
  • Mail needs SERVER_PUBLIC_URL and CLIENT_BASE_URL. With NODE_ENV=production, both must be https:// addresses on another host, not localhost.
  • Set both or neither: UPSTREAM_BASE_URL and UPSTREAM_API_KEY; PLANS_UPSTREAM_URL and PLANS_UPSTREAM_SECRET; TURNSTILE_SECRET_KEY and TURNSTILE_SITE_KEY; MEMBER_INVITE_DAILY_AI_LIMIT and MEMBER_INVITE_ALLOWANCE_DAYS; TRIAL_SCANS and TRIAL_DAILY_AI_LIMIT.
  • Set all three or none: VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY and VAPID_SUBJECT.
  • X requires Y: OPEN_SIGNUP=true requires mail. The Turnstile pair requires OPEN_SIGNUP=true. MEMBER_INVITE_LIFETIME_CAP requires the member-invite pair or MEMBER_INVITE_TRIAL=true. MEMBER_INVITE_TRIAL=true requires the trial pair and refuses the member-invite pair. The trial pair requires TRIAL_ADDRESS_PEPPER. TRIAL_DAYS and AI_TRIAL_INSTANCE_DAILY_LIMIT require the trial pair. AI_TRIAL_NETWORK_DAILY_LIMIT requires AI_TRIAL_INSTANCE_DAILY_LIMIT. TRIAL_TIME_ZONE requires TRIAL_DAYS. SYNC_NOTICE_URL requires SYNC_NOTICE.
  • Zero is refused where it would read as "off" and mean the opposite: AI_INSTANCE_DAILY_LIMIT, AI_TRIAL_INSTANCE_DAILY_LIMIT, AI_TRIAL_NETWORK_DAILY_LIMIT, TRIAL_SCANS, TRIAL_DAILY_AI_LIMIT, TRIAL_DAYS, MEMBER_INVITE_DAILY_AI_LIMIT and MEMBER_INVITE_ALLOWANCE_DAYS. Every other number must be positive too, except two that accept 0: TRUST_PROXY, and MEMBER_INVITE_LIFETIME_CAP, where 0 leaves members nothing to send.
  • Upper limits: TRIAL_SCANS 100, TRIAL_DAYS 90, TRIAL_DAILY_AI_LIMIT and MEMBER_INVITE_DAILY_AI_LIMIT 10000, SYNC_NOTICE 280 characters, INSTANCE_NAME 64 characters.
  • SYNC_SHARING, SYNC_RESEARCH, SYNC_FEEDBACK, DATABASE_SSL and MEMBER_INVITE_TRIAL accept true, false, 1 or 0. OPEN_SIGNUP accepts only true.
  • A value outside its list stops the boot: INSTANCE_LANGUAGE, NUTRIENT_REFERENCE_BASIS, LOG_LEVEL, TRIAL_TIME_ZONE and HEALTH_CONSENT_VERSION. A VAPID_SUBJECT that is not mailto: or https: stops the boot. So does an SMTP_HOST with a scheme, port or path. Malformed addresses in SERVER_PUBLIC_URL, CLIENT_BASE_URL, UPSTREAM_BASE_URL, PLANS_UPSTREAM_URL or SYNC_NOTICE_URL also stop the boot.

The inference service.

  • MODEL_PROFILE=external requires MODEL_RUNTIME_URL. With any other profile, a MODEL_RUNTIME_URL other than the bundled address stops the container.
  • A MODEL_PROFILE, FOOD_SOURCE, PROFILE or LOG_LEVEL outside its list stops the boot. A MODEL_RUNTIME_URL or EMBEDDING_RUNTIME_URL that is not an http:// or https:// address also stops the boot.
  • Counts and sizes must be positive whole numbers. LATENCY_CEILING_MS and RUNTIME_COMPLETION_TIMEOUT_MS also accept 0. IMAGE_MAX_LONG_EDGE must be at least 112, and PORT at most 65535.

Sign-up with Turnstile

By default an account comes only from an invitation. Set OPEN_SIGNUP=true on the core server, and anybody can ask for an account with their own address. The service then mails that address an invitation, and the letter proves the address works. So open sign-up needs mail, and the service does not start without it. The app shows the sign-up form only when the core server says its door is open.

A captcha stops scripts from flooding the service with requests. The core server checks a Cloudflare Turnstile captcha when you configure two keys:

  1. Sign in to the Cloudflare dashboard, open Turnstile, and choose Add widget. A free account is sufficient. Your domain does not have to use Cloudflare.
  2. Give the widget a name, add the host name of your app, such as openplate.example.com, and keep the mode Managed. Choose Create.
  3. Set the site key in TURNSTILE_SITE_KEY and the secret key in TURNSTILE_SECRET_KEY on the core server. Set both keys or neither.
  4. Recreate the core server, for example with docker compose -f <your file> up -d.

/health then publishes the site key. The app's sign-up page shows the captcha. Its submit button stays inactive until the user solves it. The secret key never leaves the core server. The service sends Cloudflare the captcha response and the secret key, not the visitor's address.

The service checks only the sign-up request, POST /v1/auth/signup-request. Signing in, opening an invitation, and all other routes carry no captcha. When the service cannot reach Cloudflare, it returns 503, and the person can try again later. The check fails closed, so an unanswered request never passes.

The app's Content-Security-Policy allows Cloudflare's captcha only on a managed instance (INSTANCE_MODE=managed), or when the newsletter form is on. On other instances, the browser blocks the captcha and nobody can submit the form. Set the app to managed before you turn the captcha on.

Without the two keys, open sign-up still works. Its other limits still apply. The service allows five requests an hour from one address, and one letter a day for one mailbox. It blocks addresses from known throwaway mail services. The service logs a warning at start.

NEWSLETTER_TURNSTILE_SITE_KEY is a separate setting. It belongs to the app and protects the newsletter form on the landing page. Its secret key stays with the service that receives that form, not with openplate. Newsletter sign-up explains it.

Building an image yourself

The Dockerfiles accept several build arguments. They are not settings for a running container. OPENPLATE_BUILD_SHA stamps the commit into the app's bundle. The inference image takes BASE_IMAGE, the llama.cpp server image to build on. It also takes NODE_IMAGE, the Node.js image for the build. VITE_ALLOWED_HOSTS applies only to the app's development server and has no default hosts.

Edit this page on GitHub