Skip to content

Environment variables

Configuration lives in .env on a self-hosted install. Set it to mode 600 and never commit it.

Generate every secret fresh, per deployment. Never copy one from another install, from this page, or from a demo.

Terminal window
openssl rand -base64 32
Variable Default Notes
NODE_ENV development Set to production for anything real
APP_VERSION Stamped in at build time. Shown under Admin, Licence
RELEASE_CHANNEL stable stable or beta. Determines which release is recommended, never causes an update
Variable Notes
DATABASE_URL PostgreSQL connection string. Required
Terminal window
DATABASE_URL="postgresql://user:pass@localhost:5432/plugboard?schema=public"
Variable Default Notes
REDIS_URL redis://localhost:6379 Optional. Only the legacy people-sync and digest worker uses it

Optional. Only needed for logos, photos and exports.

Variable Notes
S3_ENDPOINT Any S3-compatible endpoint
S3_REGION
S3_ACCESS_KEY
S3_SECRET_KEY
S3_BUCKET

The most important block, and the one most often wrong.

Variable Notes
PUBLIC_URL The origin your users type, for example https://helpdesk.yourschool.org
PUBLIC_HOST The same hostname without the scheme
API_URL The API origin
WEB_URL The web origin
API_PORT Default 4000
WEB_PORT Default 3000

PUBLIC_URL (and WEB_URL) is the CORS allow-list, the base for every emailed link, the SSO redirect target and the portal address. Set it to an internal name and you generate links your users cannot open.

https://x, https://x/ and http://x are three different values as far as CORS is concerned.

Variable Default Notes
JWT_SECRET Signs session and refresh tokens. Rotating it logs everybody out
JWT_ACCESS_TTL 15m Access token lifetime
JWT_REFRESH_TTL 30d Refresh token lifetime
Variable Notes
SECRETS_MASTER_KEY 32-byte base64. Encrypts connector credentials and backups
Terminal window
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Two options. Use a reverse proxy, or point the application at a certificate.

Variable Notes
TLS_CERT_FILE PEM certificate, including intermediates
TLS_KEY_FILE PEM private key
TLS_CA_FILE Optional intermediate chain, if your issuer ships one separately
TLS_PFX_FILE A PKCS#12 file, which is what most Windows tooling produces
TLS_PFX_PASSWORD Its password

Leave all unset to serve plain HTTP and terminate TLS at a proxy. See HTTPS and certificates.

Variable Notes
DEFAULT_TENANT_SLUG Resolves a single default tenant when no subdomain matches. For single-tenant self-hosted installs
Variable Notes
INTERNAL_API_TOKEN Shared secret the worker uses to call internal endpoints
Variable Default Notes
PORTAL_USERNAME_ENTRY 0 1 allows username-only entry at the kiosk and parent portal. Refused by default because typing a username proves nothing
PORTAL_SESSION_TTL_MS 900000 Portal session lifetime, 15 minutes
Variable Default Notes
ALLOW_PRIVATE_EGRESS 0 1 allows outbound requests to private, loopback and link-local addresses

Blocks a tenant-supplied URL (a monitor target, a webhook, a connector base URL) from being used to reach internal hosts or a cloud metadata service.

Set it to 1 on an on-premises install that legitimately monitors LAN addresses. Leave it off for anything multi-tenant.

Variable Default Notes
SSE_MAX_STREAMS_PER_USER 4 Live-update streams one signed-in person may hold open at once
SSE_MAX_STREAMS_PER_TENANT 200 Live-update streams one school may hold open at once
SSE_MAX_STREAMS 2000 Live-update streams the whole deployment may hold open at once

The console holds one live-update stream open per signed-in tab, and every open stream asks the database every 30 seconds whether the person behind it is still signed in. The number of them is therefore a load on the same connection pool every other request is queueing for.

A stream past a ceiling is refused; the console backs off and tries again, so the tab recovers on its own once one closes. Four per person covers a technician with several tabs open and one the browser has not reaped yet.

Raise these on a large desk, and raise the database connection pool with them. Setting any of them to 0 or to something unparseable means “not configured” and leaves the default in place, so live updates cannot be switched off by mistake.

Variable Notes
LICENSE_KEY Your issued licence key
LICENSE_PUBLIC_KEY The vendor’s Ed25519 public key, base64 SPKI PEM. Same for every customer
LICENSE_PUBLIC_KEYS A {kid: key} map, for key rotation
LICENSE_REVOKED_JTIS Comma-separated licence ids to refuse locally
LICENSE_SIGNING_SECRET Legacy HS256 shared secret. Needs LICENSE_ALLOW_LEGACY_HS256 as well
LICENSE_ALLOW_LEGACY_HS256 Set to 1 to allow legacy HS256 keys at all. Off by default

Instances verify with the public key. The private half lives only on the vendor, so entitlement is checked offline and a network outage cannot disable a service desk.

Your deployment holds no licence signing key, and should not be given one. The older HS256 format is symmetric — the same secret that checks a licence can issue one — so both variables above must be set, by hand, before a legacy key will verify, and neither is set for you. They exist only to keep a school running mid-migration. If you are still on a v1 key, ask us for a reissued Ed25519 one rather than setting these.

Variable Default Notes
TELEMETRY_ENDPOINT Normally comes from the licence. This is an override
DEPLOYMENT_EDITION onprem onprem or saas
USAGE_REPORTER_DISABLED 0 1 turns off reporting. Snapshots are still kept locally
USAGE_REPORTER_TICK_MS 3600000 Reporter loop interval

Only counts are reported: technician accounts, managed devices, enabled modules, version. See licence and plan.

Variable Notes
DEMO_DISABLED_MODULES Comma-separated module keys to hide on this deployment
Terminal window
DEMO_DISABLED_MODULES=module.cardChecker,module.inductions

A blunt instrument: a module hidden this way cannot be re-enabled from the interface.

Seeds the SMTP connector on first start, so a fresh install can send email before anybody opens the connectors screen.

Variable Notes
SMTP_HOST
SMTP_PORT 587 for STARTTLS, 465 for implicit TLS
SMTP_USER
SMTP_PASSWORD
SMTP_FROM For example ICT Service Desk <[email protected]>

Beyond the initial setup, email is configured as a connector.

Variable Default Notes
LOCAL_AI_URL The whole of “turn it on”. Set it and first run creates an enabled AI connector pointing at it, for every institution that has none
LOCAL_AI_MODEL qwen2.5:3b Any Ollama model on that host
OLLAMA_URL Older name, still read
OLLAMA_MODEL Older name, still read

The installer sets these for you. They are here for pointing the deployment at a model you host somewhere else.

Leave LOCAL_AI_URL blank and the desk runs without a model: the assistant falls back to its command engine, triage runs on rules, and the summary and draft features say there is no model connected.

Do not go below 3B. The 1B-class models fail the closed-list checks that triage answers are held to, often enough to be worse than no model at all.

See the bundled model, and three ways to get a model for the alternatives.

Variable Default Notes
EMBED_ORIGINS empty Parent origins allowed to frame the app, comma-separated. Empty refuses all framing

Read per request, so one image serves every deployment. Leave it unset unless you are embedding the product in another page on purpose.

Variable Default Notes
HSTS_MAX_AGE 31536000 Seconds a browser should refuse plain HTTP for this hostname. 0 sends no Strict-Transport-Security header at all

Both the API and the web app read this and send the header themselves, so it applies whatever sits in front of them — a reverse proxy, a tunnel, or nothing. Browsers only honour it over HTTPS, so the default costs a plain-HTTP install nothing and there is no need to change it there.

Set it to 0 if this hostname must stay reachable over plain HTTP by design, or if your own proxy already sends the header and you want a single authority for the value.

Note that shortening or removing it is not retroactive. A browser that has already seen a year holds it for a year from the last time it saw it; only an HTTPS response carrying a smaller max-age updates that, and only when that browser next visits.

Variable Default Notes
AGENT_JOB_TTL_MS 120000 How long a queued job waits for an agent
AGENT_LONG_POLL_MS 25000 How long an agent holds the connection with nothing to do
AGENT_TOKEN_TTL_DAYS 365 How long a newly issued or rotated agent token is accepted for

Defaults are sensible. See connector agents.

Two more are read by the agent itself, on the machine it runs on, rather than by the platform. AGENT_CONFIG and AGENT_SECRETS say where its agent.env and its secrets file are. AGENT_ALLOW_INSECURE_URL=1 lets it report to a plain http:// address, which it otherwise refuses — only set it where TLS is terminated somewhere you trust on the same host, because the agent’s token is sent on every heartbeat.

On-premises bundle only.

Variable Default Notes
AUTO_UPDATE on off to never update on its own
AUTO_UPDATE_HOUR 1 Local hour, 0 to 23
AUTO_UPDATE_RESTART on off to stage the update but apply at your next restart

Uses the machine’s own local time. A server set to UTC in a school that is not will update in the middle of a school day.

The local panel described in the status panel. On-premises bundle only.

Variable Default Notes
PANEL_HOST 127.0.0.1 The interface the panel binds to. Loopback by default
PANEL_PORT 9090 Change this if something else on the machine already has 9090
PANEL_TOKEN generated Protected installation-owner key, required on every interface including loopback. Used by the launcher to authenticate the panel; never put it in a browser URL
PANEL_ALLOWED_HOSTS (none) Extra hostnames the panel will answer to, comma separated. Needed only if you reach a non-loopback panel by name rather than by address

Open the panel with plugboard panel under the installation owner. The CLI verifies the listener and creates a one-use opening link valid for 30 seconds. The browser then needs both its session cookie and an exact-origin storage proof; the owner key is never sent to the browser. Sessions expire after eight hours or a panel restart. An empty owner key refuses authentication on loopback too. See status panel access for Linux service-owner and SSH-forwarding instructions.

Either way, the panel answers only to its own name. A request whose Host is not loopback, your PANEL_HOST, or a name you listed in PANEL_ALLOWED_HOSTS gets a 403 and nothing else — not the page, not the state, not a cookie. That is what closes DNS rebinding, where a hostile page points its own hostname at 127.0.0.1 so the browser will treat the panel as same-origin with it and send that custom header for free. Set PANEL_ALLOWED_HOSTS only for the names you actually use; it is read at start, so a change needs a restart.

It is still the wrong thing to expose. It configures the deployment and can restart it.

Variable Default Notes
BACKUP_BASE_DIR the working directory The root every backup path is contained under. The target directory set in Admin, Backups is resolved against this, and a path that lands outside it - absolute, or climbing out with .. - is refused when it is saved and again when a run starts

See backups and restore.

Variable Notes
SENTRY_DSN Error reporting to your own self-hosted Sentry or GlitchTip. No default destination
METRICS_TOKEN Unset, /api/metrics is open. Set it, and a scrape must send Authorization: Bearer <token>
BUILD_INFO_TOKEN Lets a holder read the git commit and the build timestamp from /api/health/version, which otherwise answers with the version and channel only. Unset - the default - nobody gets them. A missing or wrong token is answered with the short form rather than an error, so this can never stop a health check working

See keeping an eye on it.

Variable Notes
PLUGBOARD_DEMO_DATA 1 seeds fictional sample data on a brand new database only
WEBHOOKS_DISABLED 1 disables outbound webhook delivery
REPORTS_DISABLED 1 disables the scheduled report loop
BACKUPS_DISABLED 1 stops the scheduled backup loop from starting. Manual backups still work. Not for production