Configuration
openPRM is configured entirely with environment variables. Secrets can also be
given as a file: set NAME_FILE to its path instead of NAME (supported for
the credentials key, the enrollment code, the OIDC client secret and the
notification tokens). On
NixOS, put secrets in services.homefirst.openprm.environmentFile and plain
settings in services.homefirst.openprm.settings.
Required
Section titled “Required”| Variable | What it does |
|---|---|
DATABASE_URL | The Postgres database. openPRM creates its tables itself on start. |
OPENPRM_OWNER_PASSWORD_HASH | Bcrypt hash of the owner’s password (openPRM hash-password '<password>' prints one). Needed unless you sign in with OIDC. |
OPENPRM_CREDENTIALS_KEY | openssl rand -base64 32. Encrypts the cloud keys and connected Google accounts’ tokens. Keep it with — but separate from — your backups. |
Common settings
Section titled “Common settings”| Variable | Default | What it does |
|---|---|---|
OPENPRM_HTTP_ADDR | :8080 | Where the web UI and API listen. Use :8081 next to tendant. |
OPENPRM_PUBLIC_URL | — | openPRM’s address as your browser reaches it. |
OPENPRM_TRUSTED_PROXIES | — | Proxies (CIDRs) whose forwarded headers are trusted, e.g. 127.0.0.1,::1 behind nginx or tailscale serve. |
OPENPRM_LLM_BACKEND | ollama | The model that turns notes into facts (ollama, or stub for none). |
OPENPRM_LLM_ENDPOINT | http://localhost:11434 | The Ollama address. |
OPENPRM_LLM_MODEL | gemma4:12b | The Ollama model. |
OPENPRM_LLM_THINK | false | Let a thinking model (gemma4) think before each step: true, false, or empty for the model default. Off is several times faster. |
OPENPRM_REMINDER_CRON | hourly | When reminders are checked (6-field cron). |
OPENPRM_API_TOKEN | — | Bearer token for the ingestion API. |
OPENPRM_EMAIL_MAILDIR | — | A Maildir to watch for notes sent by email. |
OPENPRM_REQUIRE_DKIM | false | Only trust an email’s sender when DKIM/SPF passed. |
Signing in with OIDC (optional)
Section titled “Signing in with OIDC (optional)”With an OpenID Connect provider configured (Authelia, Authentik, Keycloak, Pocket ID, Google, …) the sign-in screen shows Sign in with SSO. Each person who signs in gets their own openPRM user, created on first sign-in. The owner password stays available as a fallback.
| Variable | What it does |
|---|---|
OPENPRM_OIDC_ISSUER_URL | The provider’s issuer URL (its discovery document lives under /.well-known/openid-configuration). With OPENPRM_OIDC_CLIENT_ID, this turns SSO on. |
OPENPRM_OIDC_CLIENT_ID | The client registered for openPRM at the provider. |
OPENPRM_OIDC_CLIENT_SECRET (or …_FILE) | Its secret. Leave it empty for a public client. PKCE is always used. |
OPENPRM_OIDC_REDIRECT_URL | The callback registered at the provider. Defaults to OPENPRM_PUBLIC_URL + /auth/oidc/callback. |
OPENPRM_ADMIN_GROUP | The group that makes a user an admin. |
OPENPRM_OIDC_GROUPS_CLAIM | The ID-token claim holding groups (default groups). |
Registering openPRM at the provider: create a confidential (or public)
client using the authorization-code flow. Allow the scopes openid email profile (and whatever puts groups into the ID token, if you use
OPENPRM_ADMIN_GROUP). Register exactly one redirect URI:
https://<your openPRM>/auth/oidc/callback. The desktop apps need nothing
extra. They never talk to the provider themselves; openPRM does.
How the apps sign in:
- Web: the page goes to the provider and comes back signed in, with the usual session cookie.
- Desktop (Linux, macOS, Windows): the app opens your system browser and
listens on a random port on
127.0.0.1. When the provider sends the browser back, openPRM forwards it to that port with a one-time code, never the session itself. The app trades the code atPOST /auth/oidc/exchangefor a device token and keeps it in the keystore. The code works once, expires after 60 seconds, and is bound to a PKCE secret that only the app holds. openPRM refuses any return address that isn’t a127.0.0.1or[::1]loopback.
A sign-in must finish within 10 minutes. Sign-ins that are in progress are kept in memory, so restarting openPRM during one means starting it again.
On NixOS, put the OPENPRM_OIDC_* settings (except the secret) in
services.homefirst.openprm.settings and OPENPRM_OIDC_CLIENT_SECRET in
environmentFile.
Notifications (optional)
Section titled “Notifications (optional)”| Variable | What it does |
|---|---|
OPENPRM_NTFY_URL, OPENPRM_NTFY_TOPIC, OPENPRM_NTFY_TOKEN (or _FILE), OPENPRM_NTFY_PRIORITY, OPENPRM_NTFY_TAGS | Push reminders to ntfy. |
OPENPRM_GOTIFY_URL, OPENPRM_GOTIFY_TOKEN (or _FILE), OPENPRM_GOTIFY_PRIORITY | Push reminders to Gotify. |
OPENPRM_NOTIFY_PRIVATE | Keep personal details out of pushed notifications. |
On NixOS these are services.homefirst.openprm.notify.*.
Sign-in and sessions
Section titled “Sign-in and sessions”| Setting | What it does |
|---|---|
OPENPRM_OWNER_PASSWORD_HASH | bcrypt hash of the owner password (the local sign-in). OPENPRM_OWNER_PASSWORD (plaintext) is a dev convenience hashed at startup. |
OPENPRM_API_TOKEN | Bearer token for the machine endpoints (/api/capture, /api/export, /api/import, /api/sql). |
OPENPRM_PUBLIC_URL | When it is an https:// address, requests arriving for that host get a Secure session cookie even though a reverse proxy terminated TLS. |
OPENPRM_TRUSTED_PROXIES | Comma-separated CIDRs/IPs of your reverse proxies (e.g. 127.0.0.1,10.0.0.0/8). Their X-Forwarded-Proto: https marks a request as HTTPS for the cookie’s Secure flag; the header is ignored from anyone else. |
Sessions are stored in the database (only a hash of each token), so restarting or upgrading openPRM keeps everyone signed in. A session lasts 30 days from its last use; signing out ends it. The apps keep their session in the platform keystore. The owner (and admins) can download an export and restore one from Settings → Data.
Reminder notifications (optional)
Section titled “Reminder notifications (optional)”Reminders always appear in openPRM. To also get them on your phone or desktop while openPRM is closed, point it at a ntfy topic and/or a Gotify application. Each reminder is sent once, when it comes due (birthdays and other dates, “catch up with…” cadences, follow-ups). A relay is switched on as soon as its required settings are present; you can use both at once. Delivery is best-effort: a relay that’s down is retried briefly, then logged — the reminder itself is never lost.
| Setting | What it does |
|---|---|
OPENPRM_NTFY_URL | Your ntfy server, e.g. https://ntfy.sh or https://ntfy.example.com. Needs OPENPRM_NTFY_TOPIC. |
OPENPRM_NTFY_TOPIC | The topic to publish to; subscribe to the same topic in the ntfy app. On a public server, pick a name nobody could guess or protect it with a token. |
OPENPRM_NTFY_TOKEN (or OPENPRM_NTFY_TOKEN_FILE) | Optional ntfy access token (tk_…), or user:password for basic auth. |
OPENPRM_NTFY_PRIORITY | 1 (min) … 5 (max); unset uses the server default (3). |
OPENPRM_NTFY_TAGS | Comma-separated ntfy tags / emoji added to every notification (e.g. bell). |
OPENPRM_GOTIFY_URL | Your Gotify server, e.g. https://gotify.example.com. Needs OPENPRM_GOTIFY_TOKEN. |
OPENPRM_GOTIFY_TOKEN (or OPENPRM_GOTIFY_TOKEN_FILE) | The token of a Gotify application (Gotify → Apps → Create). |
OPENPRM_GOTIFY_PRIORITY | Same 1–5 scale as ntfy, mapped to Gotify’s 1/3/5/8/10 (8+ pops up on Android); unset ⇒ 3 (Gotify 5). |
OPENPRM_NOTIFY_PRIVATE | true sends “You have a reminder in openPRM” instead of naming the person — use it with a relay you don’t run yourself. |
OPENPRM_PUBLIC_URL | Tapping a notification opens this address (see below). Unset ⇒ notifications don’t link anywhere. |
Notifications go to one topic/application for the whole household — everyone
subscribed sees every reminder. Without OPENPRM_NOTIFY_PRIVATE a reminder
names the person it’s about (e.g. “Catch up with Alice”), so that name passes
through your ntfy/Gotify server.
homefirst.cloud (optional)
Section titled “homefirst.cloud (optional)”openPRM can connect to homefirst.cloud, Home First’s hosted proxy. It works fully without it. The cloud recognizes this server by its own key pair — generated by openPRM, kept encrypted in its database, and only the public half ever leaves it — which you enroll once: by connecting openPRM to your Home First Cloud account (Settings → Home First Cloud), or with a one-time code from an operator.
| Setting | What it does |
|---|---|
HOMEFIRST_CLOUD_ADDR | The cloud’s address. Unset ⇒ openPRM stays offline. |
HOMEFIRST_SERVER_ID | This server’s id at the cloud for operator enrollment; codes are issued for it. Not needed when you link it to an account (the cloud assigns the id). |
HOMEFIRST_HOME_LABEL | This machine’s name on your account’s approval page (default: the hostname; you can change it when connecting). |
OPENPRM_CREDENTIALS_KEY (or OPENPRM_CREDENTIALS_KEY_FILE) | Base64 of 32 random bytes (openssl rand -base64 32). Encrypts the server’s keys at rest; without it the server can’t enroll. Keep it with your backups — the keys can’t be read without it. |
HOMEFIRST_CLOUD_ENROLLMENT_CODE (or …_FILE) | The one-time enrollment code. Used only while the server has no key; remove it once enrolled. |
OPENPRM_CLOUD_KEY_ROTATION_INTERVAL | How often the server key is replaced (default 720h, 30 days; 0 turns the schedule off). |
OPENPRM_CLOUD_MAINTENANCE_CRON | When the rotation check runs (6-field cron, default hourly). |
Connecting your account: in Settings → Home First Cloud, an admin chooses Connect to Home First Cloud. openPRM shows a short code and a link; open it, sign in to your Home First Cloud account and approve the access openPRM asks for (Google: contacts, for the contacts import). openPRM then enrolls by itself. You can untick access; openPRM shows an Allow button when it later needs something you didn’t approve. Disconnect removes this server from your account and revokes its keys.
Enrolling with an operator code: get a code for your server id from whoever runs your
homefirst.cloud (an operator runs pulumi stack output enrollmentCode-<server id> --show-secrets, or homefirst-cloud enroll-code <server id> on the box), set HOMEFIRST_CLOUD_ENROLLMENT_CODE and start
openPRM. The log says “enrolled”; the code is now spent and never stored, so
remove the variable — openPRM ignores it while enrolled.
Key rotation happens on the schedule above; a failed rotation is retried with growing delays. The old key is kept for the cloud’s grace period plus one day of slack, then destroyed.
If the log says the cloud no longer recognizes this server’s key
(KEY_UNKNOWN), get a new code from the operator, clear the stored keys
(DELETE FROM cloud_server_keys;), set the new code and restart. A code that
is invalid, already used or expired is reported at startup; ask for a new one.
A message about signed requests outside the cloud’s time window means the
server’s clock is off — enable NTP.
HOMEFIRST_CLOUD_TOKEN from earlier releases is no longer used; openPRM warns
while it is set.
Google Contacts import (optional)
Section titled “Google Contacts import (optional)”Needs homefirst.cloud (above) and OPENPRM_CREDENTIALS_KEY, which also
encrypts the connected Google accounts’ tokens. See
Importing Google Contacts.
| Setting | What it does |
|---|---|
OPENPRM_PUBLIC_URL | openPRM’s own address as your browser reaches it (e.g. https://prm.example.com). After you approve access at Google, homefirst.cloud sends your browser back here. Unset ⇒ the cloud shows a “you can close this tab” page instead. |
OPENPRM_GOOGLE_CONTACTS_SYNC_INTERVAL | How often each connected Google account is synced (default 3h, minimum 15m). A newly connected account syncs straight away. |
OPENPRM_GOOGLE_CONTACTS_SYNC_CRON | When openPRM checks for accounts due a sync (6-field cron, default every 10 minutes). |
OPENPRM_GOOGLE_CONTACTS_SCOPE | Who sees imported people: household (default — like everything else in openPRM, and an imported contact can join a person already in openPRM) or private (only the person who connected the account; only their private people are matched). |
The backend’s README.md and SPEC.md in the repository describe every
variable in more depth.