Configuration
There is no configuration file. The container reads a few environment variables at boot, and everything else (sign-ups, sign-in methods, email, receipt reading) is set by an admin in the app and stored in the database.
Every variable is declared and validated in one place. The server refuses to start with an invalid value and names it, rather than running half configured.
#Required
| Variable | Default | What it does |
|---|---|---|
AUTH_SECRET | none | Signs sessions and encrypts the stored secrets |
AUTH_SECRET must be at least 32 characters. Generate one with
openssl rand -base64 32 and keep it with your backups.
It signs session cookies and, through keys derived from it, encrypts what the database must not hold in clear: the SMTP password, single sign-on client secrets and authenticator app secrets. Changing it signs everyone out, and those secrets can no longer be read: an admin has to enter the SMTP password and the client secrets again, and everyone has to set up their authenticator app again.
#Locked out
SIGN_IN_METHODS overrides the sign-in methods chosen under Admin, Sign-in
methods: a comma-separated list of password, passkey, magic-link and
email-code, like SIGN_IN_METHODS=password. While it's set, those are the
only methods offered and the admin page shows them as set by the server. Use it
to get back in after turning off the only method that worked, or after moving
the instance to another address, which leaves passkeys made on the old one
useless. Remove it once you're in and have set the methods again.
#Origin
| Variable | Default in the image | When you need it |
|---|---|---|
ORIGIN | unset | Plain HTTP with no proxy; harmless anywhere |
PROTOCOL_HEADER | x-forwarded-proto | Your proxy sends the scheme in another one |
HOST_HEADER | x-forwarded-host | Your proxy sends the host in another one |
ADDRESS_HEADER | unset | To see the client's IP, not the proxy's |
XFF_DEPTH | unset | With ADDRESS_HEADER: x-forwarded-for |
PORT | 3000 | To listen on another port |
BODY_SIZE_LIMIT | 32M | To restore a backup bigger than 32 MB |
Every form post is checked against the app's public origin, and a mismatch is
refused with 403 Cross-site POST form submissions are forbidden. OpenCount
takes that origin from the request: the scheme and host your proxy forwards in
X-Forwarded-Proto and X-Forwarded-Host. When those headers are missing, the
server assumes https.
So behind an HTTPS reverse proxy that sets both headers, it just works. On plain
HTTP with no proxy, set ORIGIN to the exact URL people type, scheme and port
included, like http://192.168.1.10:3000; the check then accepts it too.
Setting ORIGIN behind a proxy does no harm, and it also fixes the base URL of
invite links and single sign-on callbacks.
Emails and passkeys need a public address they can trust, so they never use a
request's Host header. They use ORIGIN when set, otherwise the Public
address recorded at setup (or on an admin's first visit to an admin page) and
editable in Admin, Access.
PROTOCOL_HEADER, HOST_HEADER, ADDRESS_HEADER, XFF_DEPTH, PORT and
BODY_SIZE_LIMIT belong to the Bun adapter (uploads are capped at 32 MB unless
you raise it, which restoring a bigger backup needs); its
documentation
lists the rest. Only trust forwarded headers when every request reaches the
server through a proxy you control.
#Database
| Variable | Default in the image | What it does |
|---|---|---|
DATABASE_PATH | /data/opencount.sqlite | The SQLite file |
The image keeps the database on its /data volume. Outside the image the
default is ./data/opencount.sqlite, relative to the working directory. The
directory is created when missing. Migrations run at every boot, before the
worker starts; see Backups and upgrades.
#Background worker
| Variable | Default | Range | What it does |
|---|---|---|---|
WORKER_CONCURRENCY | 2 | 1 to 32 | Background jobs run at once |
WORKER_POLL_INTERVAL_MS | 1000 | 100 to 60000 | How often the worker looks for due jobs |
The worker sends emails, adds repeating expenses, builds backups and exports, imports Tricounts and prunes finished jobs and old downloads. The defaults suit any instance; there is rarely a reason to touch them.
#Logging
| Variable | Default in the image | Values |
|---|---|---|
LOG_LEVEL | info | trace, debug, info, warn, error |
LOG_FORMAT | console | json, console |
json writes one object per line for a log shipper; console writes coloured
lines for a terminal, and is the default outside the image.
#Version
| Variable | Default | What it does |
|---|---|---|
APP_VERSION | set by the release build | Shown in the footer, sent with traces and errors |
#Traces and errors
| Variable | Default | What it does |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | unset | OTLP/HTTP collector base URL; traces are off unset |
OTEL_SERVICE_NAME | opencount | service.name on every span |
OTEL_RESOURCE_ATTRIBUTES | unset | Extra resource attributes, key=value,key=value |
SENTRY_DSN | unset | Where errors are reported; off when unset |
SENTRY_RELEASE | APP_VERSION | Release attached to reported errors |
SENTRY_ENVIRONMENT | production | Environment attached to reported errors |
With none of them set, no telemetry leaves the server. See Logs, traces and errors.
#Outbound calls
Besides your SMTP and single sign-on servers, the server calls three services,
and only when someone uses the feature: api.frankfurter.dev for the exchange
rate of an expense in another currency, api.tricount.bunq.com to import a
Tricount from its share link, and api.enablebanking.com for bank sync. Block
them and those features fail, nothing else.
#Set in the app, not here
- Sign-ups, required authenticator apps or passkeys: The admin area.
- Password and passkey sign-in, single sign-on providers: Sign-in and single sign-on.
- The SMTP server: Email.
- Bank sync: each person's own Enable Banking keys, under Account, Bank sync (Personal expenses and bank sync).
- Receipt reading and its languages: Receipt reading.
This guide lives in the project repo: edit it there, and this page follows within a day.