Environment variable reference
Every Norn setting is an environment variable with the NORN_ prefix and uppercase nested keys, so
postgres.dsn is NORN_POSTGRES_DSN and source_control.github_app_id is
NORN_SOURCE_CONTROL_GITHUB_APP_ID.
Environment variables take precedence over an optional configuration file, which takes precedence
over the defaults below. Durations are written the way Go writes them: 30s, 15m, 720h. Lists
are comma-separated. Sizes are a plain number of bytes.
Nothing here is required in the sense of having no default, but a default is not always usable: the
shipped values describe a development machine on loopback addresses. Start from
required settings, and check
settings that must agree before changing anything security-related.
Deployment methods own some of these on your behalf — see
the Helm mapping or
the Compose mapping.
Application
| Setting | Default | Purpose |
|---|
NORN_APP_NAME | norn | Service name attached to every log line |
NORN_APP_ENV | development | Environment name attached to every log line |
NORN_APP_VERSION | dev | Reported by the health endpoint; the official images set it |
NORN_APP_BASE_URL | http://localhost:5173 | Public browser origin, such as https://norn.example.com |
NORN_APP_LOG_LEVEL | info | One of debug, info, warn, error |
Instance
| Setting | Default | Purpose |
|---|
NORN_INSTANCE_SIGNUPS_OPEN | true | Whether anyone may create an account |
NORN_INSTANCE_PASSWORD_AUTH | true | Whether password sign-in is offered |
NORN_INSTANCE_SELF_HOSTED | false | Marks the instance as self-hosted in the dashboard |
HTTP server
| Setting | Default | Purpose |
|---|
NORN_HTTP_ADDR | 127.0.0.1:8080 | Listen address; a container needs 0.0.0.0:8080 |
NORN_HTTP_READ_HEADER_TIMEOUT | 5s | Time allowed to receive request headers |
NORN_HTTP_READ_TIMEOUT | 30s | Time allowed to receive a request |
NORN_HTTP_WRITE_TIMEOUT | 30s | Time allowed to write a response |
NORN_HTTP_IDLE_TIMEOUT | 120s | Keep-alive idle time |
NORN_HTTP_REQUEST_TIMEOUT | 25s | Per-request budget for dashboard, SCIM, MCP, and SSO routes |
NORN_HTTP_SHUTDOWN_TIMEOUT | 20s | Time allowed to drain in-flight requests |
NORN_HTTP_MAX_REQUEST_BYTES | 4194304 | Request body cap on dashboard routes |
NORN_HTTP_CLIENT_IP_HEADER | empty | Forwarded client-address header, such as X-Forwarded-For |
NORN_HTTP_TRUSTED_PROXIES | empty | Addresses or CIDRs whose forwarded header is believed |
Attachment transfers and inbound forge deliveries carry their own limits and are not bound by
NORN_HTTP_MAX_REQUEST_BYTES.
Sessions
| Setting | Default | Purpose |
|---|
NORN_SESSION_COOKIE_NAME | norn_session | Cookie name stem; each signed-in account gets its own slot |
NORN_SESSION_COOKIE_PATH | / | Cookie path; must stay / |
NORN_SESSION_DOMAIN | empty | Cookie domain; must stay empty |
NORN_SESSION_SECURE | false | Marks cookies secure and adds the __Host- prefix |
NORN_SESSION_SAME_SITE | lax | lax or strict; none is rejected |
NORN_SESSION_KEY_PREFIX | session: | Valkey key prefix for stored sessions |
NORN_SESSION_IDLE_TIMEOUT | 168h | Inactivity after which a session ends |
NORN_SESSION_ABSOLUTE_LIFETIME | 720h | Maximum session age regardless of use |
NORN_SESSION_REFRESH_INTERVAL | 1m | How often an active session is extended |
NORN_SESSION_MAX_PER_ACCOUNT | 20 | Concurrent sessions kept per account |
Keeping the path / and the domain empty is what makes the __Host- cookie prefix valid. Changing
either drops the cookie in the browser.
PostgreSQL
| Setting | Default | Purpose |
|---|
NORN_POSTGRES_DSN | postgres://norn:[email protected]:5433/norn?sslmode=disable | Connection string; TLS is expressed inside it |
NORN_POSTGRES_MAX_CONNS | 10 | Pool ceiling |
NORN_POSTGRES_MIN_CONNS | 1 | Pool floor |
NORN_POSTGRES_MAX_CONN_LIFETIME | 1h | Maximum age of a pooled connection |
NORN_POSTGRES_MAX_CONN_IDLE_TIME | 30m | Idle time before a connection is closed |
NORN_POSTGRES_CONNECT_TIMEOUT | 10s | Time allowed to connect at startup |
A password inside the connection string must be URL-safe or percent-encoded.
Valkey
Sessions, rate limits, single sign-on state, and authorisation policy synchronisation.
| Setting | Default | Purpose |
|---|
NORN_VALKEY_ADDR | 127.0.0.1:6381 | Host and port |
NORN_VALKEY_USERNAME | empty | ACL user |
NORN_VALKEY_PASSWORD | empty | ACL password |
NORN_VALKEY_DB | 0 | Database index |
NORN_VALKEY_POOL_SIZE | 10 | Connection pool size |
NORN_VALKEY_DIAL_TIMEOUT | 5s | Time allowed to connect at startup |
NORN_VALKEY_READ_TIMEOUT | 3s | Per-command read timeout |
NORN_VALKEY_WRITE_TIMEOUT | 3s | Per-command write timeout |
Norn connects without TLS, so Valkey must be on a trusted network. Eviction must be disabled: this
is durable state, not a disposable cache.
Job queue
The same Valkey server, on a different database index.
| Setting | Default | Purpose |
|---|
NORN_ASYNQ_ADDR | 127.0.0.1:6381 | Host and port |
NORN_ASYNQ_USERNAME | empty | ACL user |
NORN_ASYNQ_PASSWORD | empty | ACL password |
NORN_ASYNQ_DB | 1 | Database index; kept apart from sessions |
NORN_ASYNQ_CONCURRENCY | 10 | Jobs processed at once by one worker |
NORN_ASYNQ_SHUTDOWN_TIMEOUT | 20s | Time allowed to finish running jobs |
NORN_ASYNQ_MAX_RETRY | 5 | Attempts before a job is archived |
Queue priorities are a map rather than a scalar and cannot be expressed as an environment variable.
Change them through a configuration file if you need to.
Worker
| Setting | Default | Purpose |
|---|
NORN_WORKER_HEALTH_ADDR | 127.0.0.1:8090 | Address serving /healthz and /readyz |
NORN_WORKER_SHUTDOWN_TIMEOUT | 10s | Time allowed to stop cleanly |
Exactly one worker runs per instance. It holds the job scheduler without leader election, so a second
worker double-fires every scheduled job.
Email
| Setting | Default | Purpose |
|---|
NORN_SMTP_HOST | empty | SMTP host; empty disables mail |
NORN_SMTP_PORT | 1025 | SMTP port |
NORN_SMTP_USERNAME | empty | SMTP user |
NORN_SMTP_PASSWORD | empty | SMTP password |
NORN_SMTP_AUTH_TYPE | none | none, plain, login, or cram-md5 |
NORN_SMTP_TLS_POLICY | none | none, opportunistic, or mandatory |
NORN_SMTP_FROM_ADDRESS | empty | Sender address, on a domain the provider has verified |
NORN_SMTP_FROM_NAME | Norn | Sender display name |
NORN_SMTP_TIMEOUT | 15s | Time allowed per delivery |
While mail is disabled, sign-up and invitation links are shown on screen instead of sent.
Storage
| Setting | Default | Purpose |
|---|
NORN_STORAGE_BACKEND | filesystem | filesystem or s3 |
NORN_STORAGE_ROOT | ./data/blobs | Directory holding attachments, with filesystem |
NORN_STORAGE_ENDPOINT | http://127.0.0.1:3900 | S3 endpoint, which the browser must be able to reach |
NORN_STORAGE_REGION | garage | S3 signing region |
NORN_STORAGE_BUCKET | norn-local | Bucket name; it must already exist |
NORN_STORAGE_ACCESS_KEY_ID | empty | S3 access key |
NORN_STORAGE_SECRET_ACCESS_KEY | empty | S3 secret key |
NORN_STORAGE_USE_PATH_STYLE | true | Path-style addressing; false for virtual-hosted buckets |
NORN_STORAGE_PUBLIC_BASE_URL | empty | Public object URL when it differs from the endpoint |
With filesystem, attachment bytes travel through the API and no bucket, credentials, or CORS policy
are involved. With s3, the browser transfers them directly, so the endpoint is part of the public
surface.
Attachments
| Setting | Default | Purpose |
|---|
NORN_ATTACHMENTS_MAX_FILE_BYTES | 26214400 | Largest single file |
NORN_ATTACHMENTS_MAX_WORKSPACE_BYTES | 0 | Per-workspace quota; 0 is unlimited |
NORN_ATTACHMENTS_UPLOAD_TTL | 15m | Lifetime of an upload authorisation |
NORN_ATTACHMENTS_LINK_TTL | 5m | Lifetime of a download link |
NORN_ATTACHMENTS_TRANSFER_TIMEOUT | 10m | Time allowed for one transfer |
NORN_ATTACHMENTS_RECLAIM_SCHEDULE | */5 * * * * | When abandoned uploads are swept |
NORN_ATTACHMENTS_RECLAIM_BATCH | 200 | Uploads swept per run |
Realtime and notifications
| Setting | Default | Purpose |
|---|
NORN_REALTIME_ENABLED | true | Whether the event stream is served |
NORN_REALTIME_MAX_PER_ACCOUNT | 12 | Concurrent streams per account, per API process |
NORN_NOTIFICATIONS_FANOUT_SCHEDULE | * * * * * | When notifications are delivered |
NORN_NOTIFICATIONS_DIGEST_SCHEDULE | */15 * * * * | When digests are sent |
Passwords
| Setting | Default | Purpose |
|---|
NORN_PASSWORD_BREACH_CHECK_ENABLED | true | Refuse passwords found in public breach corpora |
NORN_PASSWORD_BREACH_CHECK_ENDPOINT | https://api.pwnedpasswords.com/range | Range API used for the check |
NORN_PASSWORD_BREACH_CHECK_TIMEOUT | 5s | Time allowed for the check |
The check sends the first five characters of a password hash and nothing else. It needs outbound
internet access; turn it off on an isolated network.
Security and authorisation
| Setting | Default | Purpose |
|---|
NORN_SECURITY_ENCRYPTION_KEY | empty | 32 bytes of base64 encrypting stored credentials |
NORN_CASBIN_TABLE_NAME | casbin_rule | Table holding the authorisation policy |
NORN_CASBIN_WATCHER_CHANNEL | casbin:policy | Valkey channel announcing policy changes |
Without an encryption key, Norn starts but refuses to store identity provider secrets, import
credentials, source-control tokens, and webhook signing keys.
Single sign-on
| Setting | Default | Purpose |
|---|
NORN_OIDC_REQUEST_TIMEOUT | 10s | Time allowed per discovery or token request |
NORN_OIDC_MAX_RESPONSE_SIZE | 1048576 | Largest accepted provider response |
NORN_OIDC_STATE_TTL | 10m | Lifetime of an in-flight sign-in |
NORN_SAML_REQUEST_TIMEOUT | 10s | Time allowed to fetch provider metadata |
NORN_SAML_MAX_RESPONSE_SIZE | 1048576 | Largest accepted metadata document |
NORN_SAML_STATE_TTL | 10m | Lifetime of an in-flight sign-in |
NORN_SAML_REPLAY_TTL | 30m | How long a consumed assertion is remembered |
NORN_SAML_MAX_CLOCK_SKEW | 3m | Clock difference tolerated in an assertion |
NORN_SAML_MAX_ISSUE_DELAY | 90s | Age at which an assertion is too old to accept |
NORN_SAML_CERTIFICATE_SWEEP_SCHEDULE | 0 8 * * * | When expiring certificates are reported |
MCP
| Setting | Default | Purpose |
|---|
NORN_MCP_ENABLED | false | Whether /mcp is served |
NORN_MCP_REQUESTS_PER_WINDOW | 300 | Requests allowed per token per window |
NORN_MCP_RATE_WINDOW | 1m | Length of that window |
NORN_API_TOKENS_EXPIRY_SWEEP_SCHEDULE | 0 9 * * * | When expiring tokens are reported |
Webhooks
| Setting | Default | Purpose |
|---|
NORN_WEBHOOKS_FAN_OUT_SCHEDULE | * * * * * | When pending deliveries are dispatched |
NORN_WEBHOOKS_FAN_OUT_BATCH | 200 | Deliveries dispatched per run |
NORN_WEBHOOKS_REQUEST_TIMEOUT | 10s | Time allowed per delivery |
NORN_WEBHOOKS_DIAL_TIMEOUT | 5s | Time allowed to connect to a receiver |
NORN_WEBHOOKS_MAX_RESPONSE_SIZE | 65536 | Response bytes read from a receiver |
NORN_WEBHOOKS_SECRET_GRACE | 24h | How long a rotated signing secret stays valid |
NORN_WEBHOOKS_RETENTION | 720h | How long delivery records are kept |
NORN_WEBHOOKS_SWEEP_SCHEDULE | 0 5 * * * | When old delivery records are removed |
NORN_WEBHOOKS_SWEEP_BATCH | 5000 | Records removed per run |
NORN_WEBHOOKS_ALLOWED_DESTINATIONS | empty | CIDRs exempt from the private-address guard |
Norn refuses to deliver to private addresses. Each allowed destination opens a hole in that guard, so
list one only when a receiver genuinely sits on an internal network.
Imports
| Setting | Default | Purpose |
|---|
NORN_IMPORTS_CHUNK_SIZE | 25 | Records written per transaction |
NORN_IMPORTS_PAGE_SIZE | 200 | Records read per source request |
NORN_IMPORTS_SLICE_BUDGET | 45s | Work done before a slice yields |
NORN_IMPORTS_LEASE_TTL | 2m | Lease held on a slice; must exceed the budget |
NORN_IMPORTS_RESCUE_SCHEDULE | * * * * * | When abandoned slices are picked up |
NORN_IMPORTS_RESCUE_BATCH | 50 | Slices rescued per run |
NORN_IMPORTS_MAX_ATTEMPTS | 20 | Attempts before a slice fails |
NORN_IMPORTS_MIN_BACKOFF | 1s | Shortest retry delay |
NORN_IMPORTS_MAX_BACKOFF | 15m | Longest retry delay |
NORN_IMPORTS_RECORD_RETENTION | 720h | How long import ledgers are kept |
NORN_IMPORTS_MAX_ATTACHMENT_BYTES | 26214400 | Largest attachment an import may carry |
NORN_IMPORTS_MAX_UPLOAD_BYTES | 4194304 | Largest file an import may be handed |
NORN_LINEAR_ENDPOINT | https://api.linear.app/graphql | Where a Linear import reads from |
NORN_LINEAR_REQUEST_TIMEOUT | 30s | Time allowed per request |
NORN_LINEAR_MAX_RESPONSE_SIZE | 33554432 | Largest accepted response |
NORN_LINEAR_PAGE_SIZE | 100 | Issues read per request, at most 250 |
Source control
| Setting | Default | Purpose |
|---|
NORN_SOURCE_CONTROL_GITHUB_APP_ID | empty | GitHub App identifier |
NORN_SOURCE_CONTROL_GITHUB_APP_SLUG | empty | GitHub App URL slug |
NORN_SOURCE_CONTROL_GITHUB_APP_CLIENT_ID | empty | GitHub App client identifier |
NORN_SOURCE_CONTROL_GITHUB_APP_CLIENT_SECRET | empty | GitHub App client secret |
NORN_SOURCE_CONTROL_GITHUB_APP_PRIVATE_KEY | empty | GitHub App private key, in PEM form |
NORN_SOURCE_CONTROL_GITHUB_APP_WEBHOOK_SECRET | empty | Secret signing inbound deliveries |
NORN_SOURCE_CONTROL_GITHUB_ENDPOINT | https://api.github.com | GitHub API base |
NORN_SOURCE_CONTROL_GITLAB_ENDPOINT | https://gitlab.com | GitLab base |
NORN_SOURCE_CONTROL_APP_STATE_TTL | 10m | Lifetime of an in-flight app installation |
NORN_SOURCE_CONTROL_REQUEST_TIMEOUT | 30s | Time allowed per forge request |
NORN_SOURCE_CONTROL_DIAL_TIMEOUT | 5s | Time allowed to connect to a forge |
NORN_SOURCE_CONTROL_MAX_RESPONSE_SIZE | 8388608 | Largest accepted forge response |
NORN_SOURCE_CONTROL_MAX_DELIVERY_BYTES | 4194304 | Largest accepted inbound delivery |
NORN_SOURCE_CONTROL_PAGE_SIZE | 100 | Records read per forge request |
NORN_SOURCE_CONTROL_RECONCILE_SCHEDULE | */5 * * * * | When repositories are reconciled |
NORN_SOURCE_CONTROL_RECONCILE_BATCH | 20 | Repositories reconciled per run |
NORN_SOURCE_CONTROL_CALLS_PER_CYCLE | 30 | Forge calls allowed per run |
NORN_SOURCE_CONTROL_MAX_CATCH_UP | 168h | How far back a reconcile will read |
NORN_SOURCE_CONTROL_MAX_ATTEMPTS | 5 | Attempts before a delivery fails |
NORN_SOURCE_CONTROL_MIN_BACKOFF | 30s | Shortest retry delay |
NORN_SOURCE_CONTROL_MAX_BACKOFF | 1h | Longest retry delay |
NORN_SOURCE_CONTROL_DELIVERY_RETENTION | 720h | How long delivery records are kept |
NORN_SOURCE_CONTROL_ALLOWED_DESTINATIONS | empty | CIDRs exempt from the private-address guard |
Registering the GitHub App is covered in
Configure the instance GitHub App.
Audit, checks, and retention
| Setting | Default | Purpose |
|---|
NORN_AUDIT_RETENTION | 8760h | How long audit records are kept |
NORN_AUDIT_SWEEP_SCHEDULE | 0 4 * * * | When expired audit records are removed |
NORN_AUDIT_SWEEP_BATCH | 5000 | Records removed per run |
NORN_CHECKS_EXPIRY_SWEEP_SCHEDULE | 17 * * * * | When expiring checks are reported |
NORN_CHECKS_SWEEP_BATCH | 500 | Checks examined per run |
NORN_WORKSPACE_DELETION_GRACE_PERIOD | 720h | How long a deleted workspace can be restored |
NORN_CYCLES_GENERATION_SCHEDULE | 5 0 * * * | When upcoming cycles are created |
Licence and geolocation
| Setting | Default | Purpose |
|---|
NORN_LICENCE_KEY | empty | Licence enabling audit logs and directory sync |
NORN_LICENCE_GRACE | 720h | How long features keep working after expiry |
NORN_GEOIP_DATABASE_PATH | empty | MaxMind database naming session locations |
A geolocation database path that cannot be read stops the process from starting.
Settings that must agree
Norn validates its whole configuration before opening a listener and names the setting it rejected.
These are the rules that involve more than one value:
| Rule | Why |
|---|
NORN_SESSION_SECURE must be true when NORN_APP_BASE_URL is https | otherwise the session cookie travels over any plain-http request to the same host |
NORN_SMTP_HOST and NORN_SMTP_FROM_ADDRESS are set together or neither | half-configured mail fails at the moment somebody needs a password reset |
NORN_HTTP_CLIENT_IP_HEADER requires NORN_HTTP_TRUSTED_PROXIES | without an allow-list anyone could forge the header and evade the sign-in throttle |
The storage host must differ from the NORN_APP_BASE_URL host, with s3 | uploaded files would be served by an origin that holds the session cookie |
NORN_IMPORTS_MAX_UPLOAD_BYTES must not exceed NORN_HTTP_MAX_REQUEST_BYTES | the promise would be broken by a limit that does not name itself |
NORN_SOURCE_CONTROL_MAX_DELIVERY_BYTES must not exceed NORN_HTTP_MAX_REQUEST_BYTES | the same, for inbound forge deliveries |
NORN_SESSION_REFRESH_INTERVAL must be shorter than NORN_SESSION_IDLE_TIMEOUT | a session that refreshes less often than it expires can never stay alive |
NORN_SESSION_IDLE_TIMEOUT must not exceed NORN_SESSION_ABSOLUTE_LIFETIME | the shorter limit would never apply |
NORN_IMPORTS_SLICE_BUDGET must be shorter than NORN_IMPORTS_LEASE_TTL | a slice that outlives its lease is rescued while it is still running |
NORN_ATTACHMENTS_MAX_WORKSPACE_BYTES, when set, must not be below NORN_ATTACHMENTS_MAX_FILE_BYTES | no file could ever be stored |