Configuration ⚙️
Almost all runtime behavior is controlled through one YAML file: src/main/resources/config.yaml. mangoo I/O loads it with SnakeYAML, flattens the nested keys into dot notation, and exposes them through the Config class and Guice @Named bindings. There is deliberately no second configuration mechanism (no properties files, no XML, no @ConfigurationProperties classes to keep in sync); if a setting exists, it lives in this one file, which makes it realistic to actually read the whole configuration of an application in one sitting.
Typical uses include connector host and port, cookie names, MongoDB connection details, SMTP settings, feature toggles (admin UI, metrics, scheduler), and security options (CORS, authentication redirects). Sensitive values should reference environment variables, JVM properties or secret files rather than plain text in the repository, since config.yaml normally ends up committed to source control.
The file is split into a default block (shared by all modes) and an environments block with overrides for dev, test, and prod. When the application starts, it merges default with the section for the active mode, key by key, so a key you only set in default still applies everywhere, and a key you also set under environments.prod overrides it just for production. Missing an environment section for the active mode causes startup to fail on purpose. That may feel strict the first time it happens, but it exists to catch an environment block that was renamed or forgotten, where the app would otherwise silently fall back to defaults in a place where nobody is watching. Only dev, test and prod exist; any other value of application.mode selects prod.
Values are addressed with dot notation that mirrors the YAML hierarchy:
application:
api:
key: foo
config.getString("application.api.key");
You can point the application at a different file entirely by setting the JVM property application.config to an absolute path, which is handy for a config file that lives outside the JAR in production (see Operating).
Accessing configuration
Inject io.mangoo.core.Config. Constructor injection is preferred, mostly because it makes Config show up in your test setup explicitly instead of being pulled in behind the scenes:
import io.mangoo.core.Config;
import jakarta.inject.Inject;
public class MyService {
private final Config config;
@Inject
public MyService(Config config) {
this.config = config;
}
}
Look up values by key string, or by the constants in io.mangoo.constants.Key if you would rather avoid typos in raw strings scattered across the codebase:
config.getString("application.api.key");
config.getString(Key.APPLICATION_API_KEY);
Typed helpers also exist (getInt, getLong, getBoolean) and dedicated getters such as getApplicationName() or getSmtpHost() for the values the framework itself relies on.
Every entry in config.yaml is also bound as a Guice @Named string, so a class that only needs one particular value can ask for exactly that, rather than pulling in the whole Config object:
@Inject
public MyService(@Named("application.named") String named) {
// ...
}
Modes
mangoo I/O has three modes: dev, test, and prod. The mode decides which environments.<mode> block gets merged into default, so switching modes is really just switching which overrides apply, not a different code path.
- dev is activated by
mvn mangooio:run. - test is activated when tests start the application through
TestRunner. - prod is the default when you start the packaged JAR, so you do not need to set anything special for a normal deployment.
Set the mode explicitly with a system property, useful when you want prod-like behavior locally to reproduce something:
java -Dapplication.mode=dev -jar myapp.jar
or, less common outside of tooling that starts the JVM itself:
System.setProperty("application.mode", "dev");
Environment-specific values
Put shared settings under default and overrides under environments.<mode>. The active mode overwrites matching keys from default, leaving everything else untouched:
default:
application:
name: myapp
url: http://localhost
environments:
test:
application:
url: https://test.example.com
prod:
application:
url: https://example.com
Here application.name stays myapp in every mode, since none of the environment blocks touch it, while application.url changes depending on where the app is actually running.
The active environment block is required, even if it only overrides a single key. If environments.dev (or test / prod) is missing entirely, startup fails rather than quietly falling back to default alone.
Secrets, environment variables, and JVM arguments
Configuration values can also come from environment variables, JVM arguments or secret files instead of being written out in plain text. See Secrets for how env{}, arg{} and file{} placeholders are resolved. A placeholder without a value and without a fallback fails the startup, unless it is optional (env{?}), and in prod mode the framework secrets must not be literal.
As a rule of thumb: cookie secrets and keys, database and SMTP credentials should never sit in source control as clear text, since config.yaml is versioned right alongside your application code and gets the same read access as everything else in the repository.
Connectors
At least one HTTP or HTTPS connector must be configured, otherwise the application refuses to start. There is no implicit port. A connector starts only when both host and port are set. HTTPS accepts TLS 1.3 only.
default:
connector:
http:
host: localhost
port: 8080
https:
host: localhost
port: 8443
keystore: /etc/myapp/https.p12
password: file{}
HTTPS reads key and certificate chain from the PKCS12 file in connector.https.keystore, see Secrets. connector.https.certificate.alias is only needed if the file holds more than one key entry.
Default values
Keys that you omit fall back to the defaults below. Cells marked (none) have no built-in default, meaning the feature stays off or the value stays empty until you set it yourself. MongoDB keys are nested under persistence in YAML (persistence.mongo.host or persistence.mongo.url, and persistence.<prefix>.mongo.host for additional named datastores, see Persistence).
| Key | Description | Default |
|---|---|---|
application.admin.enable |
Enables the admin dashboard | false |
application.admin.locale |
Locale for the admin dashboard | en_EN |
application.admin.password |
Argon2id PHC hash of the admin dashboard password | (none) |
application.admin.secret |
Admin TOTP secret; if set, MFA is required | (none) |
application.admin.username |
Admin dashboard username | (none) |
application.allowedOrigins |
Comma-separated origins for OriginFilter |
(none) |
application.api.key |
Shared secret for ApiKeyFilter (at least 64 bytes) |
(none) |
application.language |
Default application language | en |
application.name |
Application name (JWT issuer, logs) | mangooio-application |
application.named |
Example named Guice binding | (none) |
application.parameter.strict |
Reject requests whose query parameter collides with a route parameter with a 400 |
false |
application.secret |
Application secret, required; fallback for cookie secrets and keys (exactly 64 bytes) | (none) |
application.shutdown.timeout |
Seconds from the start of shutdown, including the heartbeat stop and closing SSE and WebSocket connections, see Graceful shutdown | 20 |
application.timezone |
Application timezone | UTC |
application.validation.passthrough |
Return Bean Validation errors as JSON instead of a rendered form | false |
authentication.blacklist |
Enable revocation of authentication cookies, see Blacklist | false |
authentication.cookie.name |
Authentication cookie name | mangooio-auth |
authentication.cookie.key |
JWT signing key (min. 64 bytes); falls back to application.secret |
(none) |
authentication.cookie.secret |
JWT encryption secret (exactly 64 bytes); falls back to application.secret |
(none) |
authentication.cookie.remember.expires |
Remember-me lifetime in seconds | 2592000 |
authentication.cookie.samesitemode |
SameSite attribute | Strict |
authentication.cookie.secure |
Secure cookie flag | false |
authentication.cookie.token.expires |
Token and cookie lifetime in seconds | 3600 |
authentication.hashing.concurrency |
Maximum number of Argon2 hashes computed at the same time; 0 derives it from the configured memory cost and the heap, clamped to 2..8 |
0 |
authentication.hashing.iterations |
Argon2id iterations (time cost); minimum 2 |
3 |
authentication.hashing.memory |
Argon2id memory cost in KiB; minimum 8192 |
32768 |
authentication.hashing.parallelism |
Argon2id lanes; minimum 1 |
1 |
authentication.hashing.timeout |
Time in milliseconds a request waits for a free hashing slot | 5000 |
authentication.lock |
Failed attempts before lockout, per identifier and per step (password, second factor) | 10 |
authentication.lock.duration |
Lockout duration in minutes, absolute and not extended by further failed attempts | 60 |
authentication.origin |
Append ?origin= on auth redirects |
false |
authentication.redirect.login |
Redirect when authentication is missing | (none) |
authentication.redirect.mfa |
Redirect when MFA is required | (none) |
connector.http.host |
HTTP bind address | (none) |
connector.http.port |
HTTP port | (none) |
connector.https.host |
HTTPS bind address | (none) |
connector.https.port |
HTTPS port | (none) |
connector.https.certificate.alias |
Alias of the key entry in the keystore; only needed if it holds more than one | (none) |
connector.https.keystore |
PKCS12 file with key and certificate chain of the HTTPS connector | (none) |
connector.https.password |
Password of the keystore and its key entry | (none) |
connector.proxy |
Take client address and scheme from the X-Forwarded-* headers; only enable behind a trusted proxy that overwrites them |
false |
cors.alloworigin |
Regex for Access-Control-Allow-Origin |
^http(s)?://(www\.)?example\.(com\|org)$ |
cors.enable |
Send CORS headers | false |
cors.headers.allowcredentials |
Access-Control-Allow-Credentials |
true |
cors.headers.allowheaders |
Access-Control-Allow-Headers |
Authorization,Content-Type,Link,X-Total-Count,Range |
cors.headers.allowmethods |
Access-Control-Allow-Methods |
DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT |
cors.headers.exposeheaders |
Access-Control-Expose-Headers |
Accept-Ranges,Content-Length,Content-Range,ETag,Link,Server,X-Total-Count |
cors.headers.maxage |
Access-Control-Max-Age |
864000 |
cors.urlpattern |
Regex of request URLs that receive CORS headers | ^http(s)?://([^/]+)(:([^/]+))?(/([^/])+)?/api(/.*)?$ |
flash.cookie.name |
Flash cookie name | mangooio-flash |
flash.cookie.key |
Flash JWT signing key (min. 64 bytes) | (none) |
flash.cookie.secret |
Flash JWT encryption secret (exactly 64 bytes) | (none) |
form.maxfilesize |
Maximum size of a single uploaded file in bytes | 5242880 |
i18n.cookie.name |
Locale cookie name | mangooio-i18n |
metrics.enable |
Collect request metrics for the admin dashboard | false |
otlp.enable |
Enable OpenTelemetry export | false |
otlp.endpoint |
OTLP gRPC endpoint | (none) |
persistence.enable |
Enable MongoDB persistence | true |
persistence.mongo.auth |
Use MongoDB authentication | false |
persistence.mongo.authdb |
MongoDB authentication database | admin |
persistence.mongo.dbname |
MongoDB database name | mangoo-io-mongodb |
persistence.mongo.embedded |
Start embedded MongoDB | false |
persistence.mongo.host |
MongoDB host | localhost |
persistence.mongo.password |
MongoDB password, passed to the driver as is, so special characters need no encoding | (none) |
persistence.mongo.port |
MongoDB port | 27017 |
persistence.mongo.schema |
create creates missing collections and indexes at startup, validate only checks them, see Schema |
create |
persistence.mongo.username |
MongoDB username | (none) |
persistence.mongo.url |
mongodb:// or mongodb+srv:// connection string that replaces host and port. Credentials in the URL are rejected |
(none) |
scheduler.enable |
Enable @Run scheduling |
true |
session.cookie.expires |
Persist the session cookie beyond the browser session | false |
session.cookie.name |
Session cookie name | mangooio-session |
session.cookie.key |
Session JWT signing key (min. 64 bytes) | (none) |
session.cookie.secret |
Session JWT encryption secret (exactly 64 bytes) | (none) |
session.cookie.samesitemode |
SameSite attribute | Strict |
session.cookie.secure |
Secure cookie flag (also used for flash) | false |
session.cookie.token.expires |
Session token lifetime in seconds | 3600 |
smtp.authentication |
Enable SMTP authentication | false |
smtp.connection.timeout |
Time in milliseconds to wait for the SMTP connection. 0 waits forever and is rejected |
10000 |
smtp.debug |
Enable SMTP debug output | false |
smtp.from |
Default From address | mangoo <noreply@mangoo.local> |
smtp.host |
SMTP host | localhost |
smtp.password |
SMTP password | (none) |
smtp.port |
SMTP port | 25 |
smtp.protocol |
smtps opens TLS immediately, smtptls requires STARTTLS, smtp is cleartext and only allowed in dev and test |
smtps |
smtp.read.timeout |
Time in milliseconds to wait for an SMTP read. 0 waits forever and is rejected |
30000 |
smtp.username |
SMTP username | (none) |
undertow.maxentitysize |
Maximum HTTP entity size in bytes | 4194304 |
smtps and smtptls are the two encrypted ways to reach a mail server, and they are not interchangeable: smtps starts the TLS handshake before any SMTP command, which is what port 465 expects, while smtptls connects in cleartext and then requires STARTTLS, which is what port 587 expects. If the server does not upgrade the connection, the delivery is aborted before authentication or the message is sent. smtp leaves the connection in cleartext and the application refuses to start with it in prod mode; dev and test accept it because the local test server speaks plain SMTP. Every delivery gives up after smtp.connection.timeout milliseconds without a connection and after smtp.read.timeout milliseconds without a read. The defaults are 10 seconds and 30 seconds. Both configured values are written to the log when the mail session is created, and a delivery that hits either limit is logged again with the same values. A value of 0 or below is rejected at startup.
authentication.cookie.samesitemode and session.cookie.samesitemode default to Strict. Lax is the only other accepted value. Anything else, including None, fails startup. Flash and the locale cookie are always Strict. Strict is not sent on a cross-site navigation, such as a link from another domain. Use Lax when a login flow depends on that.
undertow.maxentitysize caps the body at 4 MiB. form.maxfilesize caps each uploaded file at 5 MiB, and the body limit is checked first, so a 5 MiB file needs a larger maxentitysize and a matching limit on the reverse proxy (client_max_body_size in nginx). A form that trips form.maxfilesize is rejected before the controller runs, logged at WARN, and answered with HTTP 400. Undertow's parameter limit (1000) and header size (8192 bytes) are not configurable. Worker and I/O thread counts are Undertow's defaults. The framework does not set them.
application.mode and application.config are JVM system properties, not YAML keys. application.secrets.path defaults to /run/secrets and is a system property or the environment variable APPLICATION_SECRETS_PATH. It cannot be set inside config.yaml. See Secrets.
authentication.cookie.token.expires has to stay at or below authentication.cookie.remember.expires. Inbound cookies are accepted only up to the remember-me lifetime. A normal login cookie whose TTL is longer than that fails to parse. The defaults, 3600 and 2592000, do not hit this.
Argon2 hashing
CommonUtils.hashArgon2 and CommonUtils.matchArgon2 compute Argon2id, and a single computation holds authentication.hashing.memory KiB of heap for its entire duration, 32 MiB with the default of 32768 KiB. Concurrent logins multiply that. The framework does not set Undertow's worker count, so the cap below is what keeps a burst of logins from allocating that memory without a bound.
The framework therefore caps how many hashes run at the same time. A call that finds every slot taken waits up to authentication.hashing.timeout milliseconds and is then rejected with a MangooHashingException, which is unchecked and does not change any signature. Authentication#isValidLogin catches it, logs it and returns false without counting a failed attempt, so an overload situation cannot lock a user out. Every other caller sees the exception and ends up in the ExceptionHandler, which answers it with a 503 rather than a silent "wrong password".
The default authentication.hashing.concurrency: 0 derives the slot count as floor((0.5 * maxMemory) / (memoryKiB * 1024 * 1.2)), clamped to 2..8. A configured value greater than 0 is used as is. The effective value is logged at INFO during startup. A high explicit value is close to having no gate.
The defaults m=32768, t=3, p=1 sit above the OWASP minimum of 19 MiB and two iterations and below RFC 9106's low-memory profile. p=1 is deliberate: BouncyCastle's Argon2BytesGenerator computes the lanes sequentially, so a higher parallelism does not shorten a single hash, it only spreads the same memory over more lanes. A server gets its parallelism from concurrent requests, not from lanes inside one hash.
Values below authentication.hashing.memory: 8192, authentication.hashing.iterations: 2 or authentication.hashing.parallelism: 1 are rejected with an IllegalArgumentException at startup. The application refuses to start rather than hashing weaker than intended. mvn -q mangooio:hash creates the admin password hash with these three values, see Administration.
Changing the parameters is safe: a hash carries the parameters it was created with, see Password hashing, and is always verified with those. CommonUtils.needsRehash reports hashes that were created with anything else than the current configuration.