Configuration reference — Versola Docs
VersolaVersola/docs
Pre-1.00.7.1versola.kzGitHub

Configuration reference

How auth and central load their HOCON configuration, the environment variables they read, every config block with keys and defaults, Postgres TLS rules and JVM defaults.

auth (data plane) and central (control plane) each read one HOCON file at startup and a handful of environment variables. This page lists both, block by block, and says which blocks a production deployment needs. edge has its own block list; see Edge configuration.

You normally do not write these files by hand. versola configure (and the Helm chart) generate them with versola-tools for the release you deploy, with secrets as ${VAR} placeholders. They are regenerated on every configure, so on the vps target a hand edit is lost; there is no supported way yet to change an individual setting there. Treat this page as a reference for what the generated files contain.

How configuration is loaded

  • The service reads the HOCON file at the path in CONFIG_PATH (container default /app/config/env.conf; the image passes it to the JVM as -Denv.path). As an alternative, the whole configuration can be passed inline in the ENV_CONFIG environment variable.
  • Keys are written in kebab-case (maximum-pool-size). Durations are HOCON durations ("60 seconds", 5 minutes).
  • Standard HOCON substitution applies, so ${VAR} and ${?VAR} take values from environment variables. The generated files use this for secrets: versola configure resolves each secret from OpenBao into a <service>.secrets.env file that Compose passes to the container as environment, and the configuration file only holds the placeholder. On Kubernetes the same keys come from a Secret (Kubernetes).
  • A syntax error stops the service at startup. HOCON cannot read UTF-16: keep hand-edited files UTF-8.
  • env must be set. The value prod marks production; any other value is treated as a test environment. Production is stricter in places noted below (for example, Postgres TLS).

Environment variables

VariableDefaultMeaning
PORT8080Application port.
DPORT8081Diagnostics port: /metrics, /liveness, /readiness. See Observability.
APORT8082Additional internal application port. Used by auth for Account Settings; not a public surface.
MPORT8083auth mutual-TLS listener. Serves only if the mutual-tls block exists.
BIND_HOST0.0.0.0Address the listeners bind to. The vps Compose file sets 127.0.0.1.
CONFIG_PATH/app/config/env.confPath of the HOCON file (images).
ENV_CONFIGnoneInline HOCON; used instead of the file when set.
RUN_MIGRATIONSfalsetrue: apply Flyway migrations at startup. false or unset: validate the schema and refuse to start on an out-of-date one. Any value other than exactly true/false stops the service. Deployments use versola migrate instead.
JAVA_OPTSsee JVM defaultsJVM flags. Setting it replaces the defaults entirely.
WARMUP_ENABLEDtrueRun the warm-up before the service reports ready. Exactly true or false.
WARMUP_BUDGET_SECONDS20Upper bound for the warm-up; after it readiness proceeds anyway.
HTTP_CLIENT_POOL_KINDfixedPool of the outbound HTTP client shared by all three services: fixed or dynamic. See Outbound HTTP client pool. An invalid value falls back to the default.
HTTP_CLIENT_POOL_SIZE128Connections per target. For dynamic, the maximum.
HTTP_CLIENT_POOL_MIN0dynamic only: connections kept open when idle.
HTTP_CLIENT_POOL_TTL30sdynamic only: how long the pool must be unused before it shrinks to the minimum, for example 30s or 2m.
ARGON2_MAX_CONCURRENCYnoneauth only: overrides argon2.max-concurrent in the generated file.
POSTGRES_POOL_METRICS_INTERVALnoneOverrides postgres.pool-metrics-interval in the generated file.

The generated configuration also reads a few variables the chart or Compose file sets for you (for example the certificate issuer on Kubernetes). Anything else, including secrets, lives in the HOCON file and the secret store.

Blocks shared by every service

Top level

KeyRequiredMeaning
envyesEnvironment name; prod enables production checks.
configuration-cache-refresh-intervalauth, centralHow often a cache of central’s configuration is refreshed. Generated: 5 minutes for vps and Kubernetes, 1 minute locally.
otel-exporternoOTLP gRPC endpoint for traces, for example http://otel-collector:4317. Absent: tracing is off. The generated files carry it commented out.

postgres

Every key except notifications-url, ssl-root-cert and pool-metrics-interval is required and has no default, so that the connection budget of all instances can be planned against Postgres max_connections. The values below are what versola-tools generates.

KeyGeneratedMeaning
urlper serviceJDBC URL. Standard topology: a database per service, so each service has its own URL, user and password. The vps and local targets generate the simplest one, a single database auth with a schema per service: ...?currentSchema=auth (or central, edge). Never point two services at the same schema.
userrole nameDatabase role.
password${POSTGRES_PASSWORD}Password.
maximum-pool-sizeauth 10, central 15HikariCP pool maximum.
minimum-idleequal to maximumIdle connections kept; must not exceed maximum-pool-size.
connection-timeout30 secondsWait for a pooled connection before failing.
max-lifetime30 minutesConnection lifetime; keep it below any server or proxy timeout.
leak-detection-threshold60 secondsA connection held longer than this logs a warning. 0 seconds disables.
notifications-urlabsentDirect (non-pooled) JDBC URL used only for the LISTEN connection. Falls back to url.
ssl-root-certabsentPath to a CA bundle that verifies the database certificate. Absent: the JVM trust store.
pool-metrics-intervalabsentPublish pool gauges (db_client_connection_*) at this interval, for example 10 seconds. Absent: none are published.

Postgres TLS

Every connection to Postgres is opened with sslmode=verify-full unless the URL sets its own sslmode: the certificate must chain to a trusted CA and match the host name. This applies to the pool and to the notifications-url connection.

  • To trust a private CA, mount its bundle into the container and set ssl-root-cert; on the vps target pass --postgres-ca <file> to versola configure and the CLI does both. A sslrootcert already in the URL wins.
  • A URL that does not verify (sslmode=disable, allow, prefer or require, or ssl=false) logs a warning at startup. With env = "prod" the service refuses to start.
  • Hosts localhost, 127.0.0.1 and [::1] are exempt from both. versola-tools writes sslmode=disable for the bundled local Postgres and for a vps whose --postgres-host is loopback; a remote vps host keeps the verified default.
  • An sslfactory in the URL that may skip validation is reported the same way.

Behind a connection pooler

central and edge hold a connection parked on LISTEN: that is how a configuration change reaches every replica and how a revoked token stops being accepted before it expires. A pooler in transaction mode cannot carry LISTEN. When url points at a pooler, set notifications-url to a direct Postgres URL:

postgres {
  url               = "jdbc:postgresql://pgbouncer:6432/auth?currentSchema=edge"
  notifications-url = "jdbc:postgresql://postgres:5432/auth?currentSchema=edge"
}

A service checks at startup that it can receive its own notification and refuses to start, naming this setting, if it cannot.

auth

Required in production: env, configuration-cache-refresh-interval, security, jwt, central, postgres. Optional: bootstrap, par, dpop, argon2, mutual-tls, otp-provider, smtp, cleanup. The generated file includes bootstrap, par, argon2, mutual-tls and cleanup.

security

Eleven secrets, all required. Each is a base64url value; the generated placeholders and the decoded length are:

KeySecret variableBytes
access-tokens-secretACCESS_TOKENS_SECRET32
client-secrets-secretCLIENT_SECRETS_SECRET16
refresh-tokens-secretREFRESH_TOKENS_SECRET32
auth-codes-secretAUTH_CODES_SECRET32
sessions-secretSESSIONS_SECRET32
passwords-secretPASSWORDS_SECRET16
conversation-cookie-secretCONVERSATION_COOKIE_SECRET32
session-cookie-secretSESSION_COOKIE_SECRET32
user-agent-cookie-secretUSER_AGENT_COOKIE_SECRET32
par-requests-secretPAR_REQUESTS_SECRET32
dpop-nonces-secretDPOP_NONCES_SECRET32

Some of these protect stored data and must never be regenerated; see Secrets.

jwt

KeyMeaning
issuerThe issuer string and public origin. Generated from --auth-url. Tokens, discovery metadata and the DPoP htu check use it, never the Host header.
private-keyRSA private key (PKCS#8 DER, standard base64), ${JWT_PRIVATE_KEY}.

central

KeyMeaning
urlAddress of central. auth syncs its configuration from it and blocks (up to two minutes) until it is reachable.
secret-keyKey shared by auth and central, ${CENTRAL_SECRET_KEY}.

par

Optional. Pushed authorization requests (RFC 9126).

KeyDefaultMeaning
request-uri-ttl60 secondsLifetime of a request_uri.
max-request-size8192Maximum size of a pushed request, in bytes.

dpop

Optional. Defaults apply when the block is absent.

KeyDefaultMeaning
iat-leeway60 secondsAllowed distance between a proof’s iat and the current time, in either direction. Also the window a proof is remembered for replay detection.
nonce-ttl300 secondsHow long a server-issued DPoP-Nonce is accepted.

Whether a nonce is required is a per-tenant setting in the admin console, not configuration. See Sender-constrained tokens.

mutual-tls

Optional. Starts a second listener on MPORT that requires a client certificate and serves /token, /introspect, /revoke, /par and /userinfo, and makes discovery advertise mtls_endpoint_aliases (RFC 8705 §5). Without the block, a client certificate reaches auth only as a header forwarded by a tenant’s proxy.

KeyRequiredMeaning
certificateyesPEM path of the certificate the listener presents.
private-keyyesPEM path of its key, unencrypted PKCS#8.
trusted-certificatesyesPEM path of the CA certificates that client chains are validated against. There is deliberately no default: falling back to the JDK trust store would let every public CA vouch for clients.
external-urlyesAddress clients are told to use for this listener (the alias in discovery). May differ from where it is bound.

The vps and Kubernetes targets generate this block and the certificates behind it (a step-ca based mtls-init service, or cert-manager). See Edge client certificates.

argon2

Optional. Argon2id password hashing.

KeyDefaultMeaning
max-concurrent12Maximum concurrent password hashes. Each holds about 19 MiB of heap while it runs, so the worst case is roughly max-concurrent x 19 MiB (about 228 MiB at 12), sized for the default 512 MiB heap. Raise it only together with the container’s memory.

otp-provider

Optional, commented out in the generated file. An HTTP endpoint that delivers OTP codes (SMS, for example).

KeyMeaning
methodHTTP method, for example POST.
urlProvider endpoint.
username, passwordOptional basic credentials.
bodyMap of request body fields; values may use the {{phone}} and {{message}} placeholders.

smtp

Optional, commented out in the generated file. Used for e-mail OTP and password delivery.

KeyMeaning
host, portSMTP server, port is typically 587.
username, passwordCredentials.
fromSender address.
subjectSubject line of the message.
start-tlstrue to upgrade the connection with STARTTLS.

Without SMTP or an OTP provider, a production deployment has no channel to deliver a temporary password; in prod a password can only be delivered, not shown on screen.

bootstrap

Optional. On a first start, creates the admin user.

KeyMeaning
loginAdmin login, admin by default.
passwordTemporary password, ${ADMIN_BOOTSTRAP_PASSWORD} on vps and Kubernetes. Valid for 24 hours; the first login forces a new one. Restarting auth after it expires sets a fresh one.
admin-user-idUUID of the admin user. Must match central’s bootstrap.admin-user-id.

cleanup

Periodic deletion of expired rows. The generated file lists these tables with their batch size and interval:

Tablebatch-sizeintervalkey-column
auth_conversations10005 minutesnone
authorization_codes10005 minutescode
pushed_authorization_requests10005 minutesrequest_uri
refresh_tokens100010 minutesnone
sso_sessions100010 minutesnone
challenge_throttle10005 minutesctid
user_passwords100012 hoursnone
user_agents100010 minutesnone

A failed batch no longer stops cleanup of that table until the next restart (fixed in 0.7.0). The service publishes cleanup_* metrics so you can see whether removal keeps up; see Observability.

KeyMeaning
max-threadsCleanup worker threads (2).
tables[].table-nameTable to clean.
tables[].batch-sizeRows per batch (1000).
tables[].intervalHow often it runs.
tables[].key-columnOptional column used to select batches (code, request_uri or ctid for some tables).

central

Required in production: env, configuration-cache-refresh-interval, secret-key, client-secrets-secret, auth, user-outbox, postgres. Optional: bootstrap, client-certificates, client-certificate-authority.

KeyMeaning
secret-keyKey shared with auth, ${CENTRAL_SECRET_KEY}.
client-secrets-secretAES key central encrypts client secrets, signing keys, edge keys and resource secrets with, ${CLIENT_SECRETS_SECRET}. Identical to auth’s. Never regenerate it.
auth.urlInternal address of auth, used for its admin calls. Defaults to the public URL in the generated file.
user-outboxHow user changes are propagated to auth: poll-interval (1 second), batch-size (32), lease (1 minute), max-backoff (5 minutes), max-attempts (5). After max-attempts failures an event moves to central.user_outbox_dead and is not retried.

bootstrap

Optional in the code; the generated file always includes it. It seeds central’s database once, on the first start against an empty database. Changing it later has no effect: edit the objects in the admin console instead.

KeyRequiredMeaning
loginyesAdmin login.
admin-user-idyesUUID of the admin user; same as auth.
redirect-urisyesRedirect URIs of the central-admin client. The generated list holds <auth-url>/central/admin/ and the edge’s /complete URL.
edgesnoEdges to register: [{ id, public-key-jwk }].
jwksnoPublic JWK set matching auth’s jwt.private-key, ${JWKS_JSON}.
metadatanoSeed of the authorization server metadata document (endpoints, supported algorithms). auth derives some fields when it serves the document; request_uri_parameter_supported is always true (since 0.7.0).
presetsnoAuthorization presets, for the admin console login: id, description, redirect-uri, post-login-redirect-uri, post-logout-redirect-uri, cookie-domain, cookie-path.
central-urlnoAddress of central.
resource-secretnoSecret of the central resource, ${CENTRAL_RESOURCE_SECRET}. Generated when absent.
front-channel-logout-urinoPublicly reachable front-channel logout endpoint, typically <auth-url>/logout/frontchannel.
passkeyyesrp-id and origins[] for passkeys.
auth-additional-urlyesInternal address of auth’s APORT listener.
auth-resource-secretyesShared secret with that listener, ${ACCOUNT_RESOURCE_SECRET}. Not auto-generated by central.
utility-clientnoThe utils client: client-id, and either public-key-jwk (private_key_jwt with DPoP) or secret. One is required; if both are given the key wins. Seeded once. See Secrets.
central-admin-mtlsnocertificate-header and certificate-encoding (urlEncodedPem or base64Der) say where a TLS terminator forwards the certificate edge presents as central-admin (RFC 8705 tls_client_auth). certificate (one PEM with the certificate and its PKCS#8 key) is optional: without it central issues the certificate itself through client-certificates (or client-certificate-authority) and refuses to boot when no CA is configured. The vps and local targets generate the block with the header ssl-client-cert and no certificate. Without the whole block central-admin is seeded on a client_secret, which the default FAPI 2.0 tenant does not admit, and bootstrap logs a warning.

client-certificates

Optional. How central issues and renews certificates for edge-fronted clients. central holds no CA key in this mode. Exactly one backend is configured; the block is read as absent unless a backend is named.

KeyDefaultMeaning
validity14 daysLifetime requested for each certificate.
renew-before4 daysA certificate with less than this left is renewed.
check-interval1 hourHow often stored certificates are examined.
step-caabsentstep-ca backend: url, root-certificate (PEM path), provisioner (JWK provisioner name), provisioner-key (JWK file path). Generated for vps and local.
cert-managerabsentcert-manager backend: issuer-name, issuer-kind (ClusterIssuer), issuer-group (cert-manager.io), namespace (the pod’s own), api-url, token-path, ca-path, namespace-path. Generated for Kubernetes.

client-certificate-authority

Optional, local development only: central holds the CA key itself. certificate and private-key are PEM paths (key unencrypted PKCS#8); validity-days optionally caps the lifetime. Without it, a registration that asks central to issue a certificate is refused. Prefer client-certificates anywhere else.

Startup order

auth and edge sync their configuration from central while they start. If central is not listening yet, they wait up to two minutes. The wait recovers by itself only with HTTP_CLIENT_POOL_KIND=dynamic. With the default fixed pool the first request opens all of the pool’s connections, central refuses them, and the pool keeps a failed connection per slot (128 by default), so each retry consumes one and the wait cannot succeed even after central is up. The service exits with code 1 after the two minutes and relies on the restart policy (restart: unless-stopped in the Compose bundles, restartPolicy: Always in Kubernetes); the restarted process builds a fresh pool and starts normally. Expect one restart on a cold install where central is not first. versola up starts central first, so it avoids this. While waiting, /liveness answers 200 and /readiness answers 503. A central that answers and rejects the request (wrong URL, wrong key) fails startup within seconds.

Outbound HTTP client pool

auth, central and edge share one outbound HTTP client: calls between the services and edge’s calls to the backends it proxies. Its pool is kept per target (scheme, host and port), so each backend gets its own pool of the configured size. The resolved pool is logged once at startup (outbound HTTP client pool: ...).

VariableDefaultMeaning
HTTP_CLIENT_POOL_KINDfixedfixed: exactly SIZE connections per target, all opened when the target is first used and kept. dynamic: between MIN and SIZE, more on demand.
HTTP_CLIENT_POOL_SIZE128Connections per target; for dynamic the maximum.
HTTP_CLIENT_POOL_MIN0dynamic only; not above the maximum.
HTTP_CLIENT_POOL_TTL30sdynamic only: the pool shrinks back to MIN only after it has seen no checkout or return for this long. It is a pool-wide idle time, so under steady traffic the extra connections stay open.

An HTTP/1.1 connection serves one request at a time, so a pool caps a pod’s throughput towards one backend at roughly size / backend latency. Neither kind fails when exhausted: a request waits for a free connection with no timeout of its own, so a pool that is too small shows up as latency, never as errors. http_client_active_requests counts outbound requests in flight, queued ones included, and rises first. Size the pool for peak requests per second per pod times the backend’s p99 latency, with headroom. A fixed pool of 128 opens 128 connections to every backend a pod touches; with many backends dynamic with a low MIN uses fewer sockets, at the cost of a connection set-up when a burst arrives.

Login state tables are UNLOGGED

This is one of Versola’s deliberate trade-offs; see Compromises for the full list of UNLOGGED tables, what a crash costs and how to turn logging back on.

Since 0.7.0, the auth_conversations, authorization_codes and pushed_authorization_requests tables are UNLOGGED: they are not written to the write-ahead log. They hold only minutes-old login state, and logging them made the database wait on WAL commits. The trade-off:

  • A clean Postgres restart keeps their rows.
  • A Postgres crash (kill -9, host failure, power loss) empties them. A standby never receives them, so after a failover they are empty, and a restore from a backup brings them back empty.
  • What is lost is in-flight state only: users in the middle of a multi-step sign-in start over, and an authorization code or pushed request that was not yet used is rejected, so the client retries. Sessions, tokens, consents and users are unaffected.
  • The migration runs ALTER TABLE ... SET UNLOGGED, which takes an exclusive lock and rewrites each table. The tables are small, but run versola migrate at low traffic: logins on a still-running older version wait while it runs.

Refresh token lifetime

The lifetime of a refresh token is fixed when it is issued and no longer slides with use (since 0.7.0). It is not a configuration-file setting: it is the refreshTokenTtl of each client, in seconds, set when the client is registered through the Central API or the admin console. When omitted, the default is 15552000 (180 days); releases before 0.7.0 used 90 days and extended it on every use.

Security headers

All application listeners of auth, central and edge (main, additional, mutual-TLS; not the diagnostics port) add these response headers unless the handler already set them:

HeaderValueWhen
X-Content-Type-Optionsnosniffalways
Referrer-Policyno-referreralways
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=()always
Content-Security-Policyframe-ancestors 'none'; base-uri 'none'; object-src 'none'HTML responses
X-Frame-OptionsDENYHTML responses
Strict-Transport-Securitymax-age=31536000; includeSubDomainsrequests that arrived over https: the URL scheme, X-Forwarded-Proto: https, or the mutual-TLS listener

There is no default-src Content-Security-Policy or form-action: they would break tenant logos and fonts and redirects to a client’s redirect_uri. Your own TLS-terminating proxy must forward X-Forwarded-Proto for HSTS to be emitted, and includeSubDomains assumes every host under the domain is https-only.

JVM defaults

The auth, central and edge images set JAVA_OPTS to a fixed-size profile on Temurin 25, so memory use does not depend on how much the host has:

-XX:+UseG1GC -Xms512m -Xmx512m -XX:MaxDirectMemorySize=64m -XX:MaxMetaspaceSize=128m
-XX:ReservedCodeCacheSize=64m -Xss512k -XX:NativeMemoryTracking=summary
-XX:+UseCompactObjectHeaders -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/diagnostics/heap.hprof -XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/diagnostics/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
--enable-native-access=ALL-UNNAMED

edge sets MaxDirectMemorySize=96m; auth and central use 64m. The heap is 512 MiB, fixed (-Xms equals -Xmx). Budget about 850 MiB of resident memory per service, which is what the chart’s 1 GiB memory limit is sized from. On an OutOfMemoryError the JVM writes one heap dump (overwritten on each crash), exits so the supervisor restarts it, and leaves a rotated GC log in /diagnostics; both deployments mount a volume there. See Observability.

Setting JAVA_OPTS replaces the whole string, so repeat any flag you still want. The older -XX:MaxRAMPercentage=75.0 default is gone: on the vps target nothing limits container memory, so a percentage would be taken from the whole server for each of three JVMs.

Next steps