Config & env
Applications need settings that change between machines: the app’s name, whether debug output is shown, which port to listen on, database credentials. Kona separates these into two layers:
- Environment (
org.konaframework.core.Env): flat string variables from the operating system, with an optional.envfile for development. This is where machine-specific and secret values belong. - Config (
org.konaframework.core.Config): a structured tree of typed values, addressed with dotted keys such asapp.debug. It is filled from environment-derived defaults, executable config modules underconfig/, and optionalconfigurecallbacks.
Your code should mostly read config. Environment variables are the raw input that seeds it.
Where values come from, in order
Section titled “Where values come from, in order”When you boot with Kona.create(...), Kona fills config in this order. A later step overwrites keys set by an earlier one.
-
.envis loaded from the base path, if the file exists. Missing files are skipped silently. -
Defaults are seeded from the environment:
Config key Environment variable Default when unset app.nameAPP_NAME"Kona"app.envAPP_ENV"local"app.debugAPP_DEBUG(parsed as a boolean)falseapp.urlAPP_URL"http://127.0.0.1:4000"app.keyAPP_KEYunset unless present -
Registered
ConfigFilemodules merge in registration order. Each module returns a map that lands under its key (app,database,session, …). Modules may callEnv.get/Env.getOrDefaultwhile building that map. Path overrides from apathsmodule are applied when the layout refreshes after this step. -
Your
configurecallbacks run, in the order you added them.
So a config.App module that sets "name" from APP_NAME can refine the seeded default, and a configure { it.set("app.name", ...) } overrides both. Applications built with Application.builder (without Kona.create) skip steps 1 to 2 unless you register the same bootstrappers yourself; they still merge any .configs(...) you add.
Config is filled once, at startup. Editing .env or a config module’s source while the app runs has no effect until restart (and recompile for modules).
Executable config modules
Section titled “Executable config modules”Put Kotlin or Java classes in the language root config/ package (next to app/, bootstrap/, routes/):
src/main/kotlin/config/ App.kt → key "app" Database.kt → key "database" Session.kt → key "session" Filesystems.kt → key "filesystems" Http.kt → key "http" Paths.kt → key "paths"Each module implements org.konaframework.core.ConfigFile with two methods:
key()— top-level config key (usually the file stem in lowercase:App→"app").config()— nested map of values for that key. CallEnvhere for machine-specific values.
Register them explicitly from bootstrap.App (reliable and easy to test):
Kona.create(args) .configs(config.App, config.Database, config.Session, config.Filesystems, config.Http, config.Paths) .providers(/* ... */) .boot()package config
import org.konaframework.core.ConfigFileimport org.konaframework.core.Env
object Database : ConfigFile { override fun key(): String = "database"
override fun config(): Map<String, Any?> = mapOf( "default" to Env.getOrDefault("DB_CONNECTION", "main"), "connections" to mapOf( "main" to mapOf( "url" to Env.getOrDefault("DB_URL", "jdbc:h2:mem:kona;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE"), "username" to Env.getOrDefault("DB_USERNAME", "sa"), "password" to Env.getOrDefault("DB_PASSWORD", ""), ), ), )}Kona.create(args) .configs( new config.App(), new config.Database(), new config.Session(), new config.Filesystems(), new config.Http(), new config.Paths() ) .providers(/* ... */) .boot();package config;
import org.konaframework.core.ConfigFile;import org.konaframework.core.Env;import java.util.LinkedHashMap;import java.util.Map;
public final class Database implements ConfigFile { @Override public String key() { return "database"; }
@Override public Map<String, Object> config() { Map<String, Object> main = new LinkedHashMap<>(); main.put("url", Env.getOrDefault("DB_URL", "jdbc:h2:mem:kona;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE")); main.put("username", Env.getOrDefault("DB_USERNAME", "sa")); main.put("password", Env.getOrDefault("DB_PASSWORD", ""));
Map<String, Object> connections = new LinkedHashMap<>(); connections.put("main", main);
Map<String, Object> values = new LinkedHashMap<>(); values.put("default", Env.getOrDefault("DB_CONNECTION", "main")); values.put("connections", connections); return values; }}You can also build a module inline with ConfigFile.of("cache", () -> Map.of("driver", "memory")).
kona new scaffolds these modules and wires .configs(...) in bootstrap.App for both Kotlin and Java (and every starter kit overlay).
There is no JSON or .properties config directory loading. Settings live in source next to the rest of the app so they can call Env and stay type-checked.
PathLayout still honors paths.langRoot, paths.resources, paths.storage, and paths.public. Ship them from config/Paths (or omit the module and keep framework defaults). Application.configPath() / Paths.config() resolve to the language-root config/ directory (for example src/main/kotlin/config), not src/main/resources/config.
Environment variables and .env
Section titled “Environment variables and .env”Env.get checks the real process environment first and falls back to values loaded from .env. This precedence is deliberate: a deployment platform, CI job, or container can inject production values and secrets without the file being edited or even present.
val host = Env.getOrDefault("HOST", "127.0.0.1")val region = Env.get("REGION") // String? — null when unsetval apiKey = Env.require("PAYMENTS_API_KEY") // throws when unsetString host = Env.getOrDefault("HOST", "127.0.0.1");String region = Env.get("REGION", null); // null when unsetString apiKey = Env.require("PAYMENTS_API_KEY"); // throws when unset| Method | Behavior |
|---|---|
get(key, default) |
Process env, then .env, then default (Kotlin may omit default; Java passes null) |
getOrDefault(key, default) |
Same, with a non-null result |
require(key) |
Same, but throws IllegalStateException("Missing environment variable [KEY]") when absent |
load(path) |
Parse a .env file and remember its values (later loads overwrite earlier keys) |
set(key, value) |
Set a fallback value in code; real process variables still win |
clear() |
Forget loaded and set values; also done by Kona.reset() |
Env is process-wide, not per application. Tests that boot several applications should call Kona.reset() between them.
.env file format
Section titled “.env file format”# Comments start with #APP_NAME=billingAPP_DEBUG=onexport PORT=9000 # an "export " prefix is allowedGREETING="Hello # not a comment"SINGLE='single quotes work too'EMPTY=- One
KEY=valueper line. Whitespace around the key and value is trimmed. - Blank lines and lines starting with
#are skipped, as are lines without=or with nothing before it. - In an unquoted value,
#(space then hash) starts a trailing comment. Inside single or double quotes,#is literal. - Surrounding quotes are removed. There is no variable interpolation, escape processing, or multi-line value support.
Boolean values
Section titled “Boolean values”APP_DEBUG, and anything you read with getBoolean, accepts these strings in any letter case:
| True | False |
|---|---|
true, 1, yes, on |
false, 0, no, off, empty string |
Numbers count as true when non-zero. For APP_DEBUG, any other string (for example maybe) is treated as false. With app.debug on, the HTTP layer adds exception messages and class names to error responses; keep it off on public servers.
The Config API
Section titled “The Config API”Config stores nested maps. A dotted key walks into the tree: set("app.debug", true) creates { "app": { "debug": true } }, and get("app") returns the whole app map.
val config = app.config
config.set("mail.host", "smtp.example.com")config.set("mail.port", 587)config.set("mail", mapOf("from" to "noreply@example.com")) // replaces the mail subtree
val name = config.getString("app.name", "Kona")val port = config.getInt("server.port", 8080)val debug = config.getBoolean("app.debug", false)val hasMail = config.has("mail.from") // trueval mail: Map<String, Any?>? = config.get("mail")Config config = app.getConfig();
config.set("mail.host", "smtp.example.com");config.set("mail.port", 587);config.set("mail", Map.of("from", "noreply@example.com")); // replaces the mail subtree
String name = config.getString("app.name", "Kona");int port = config.getInt("server.port", 8080);boolean debug = config.getBoolean("app.debug", false);boolean hasMail = config.has("mail.from"); // trueMap<String, Object> mail = config.get("mail");| Method | Returns |
|---|---|
set(key, value) |
Stores value, creating intermediate maps. Map values are copied into the tree. Setting below a non-map value replaces it with a map. |
has(key) |
true if the full path exists, even when the stored value is null |
get(key) / get(key, default) |
The raw value (or nested map), cast to the type you assign to; default only when the key is missing |
getString(key) / getString(key, default) |
toString() of the value |
getInt(key) / getInt(key, default) |
Numbers truncated to Int; numeric strings parsed; anything else counts as missing |
getBoolean(key) / getBoolean(key, default) |
Using the boolean rules above; unrecognised strings count as missing |
all() |
A deep copy of the whole tree, safe to modify |
loadMap(prefix, map) |
Deep-merge a map under prefix (empty prefix merges at the root) |
load(ConfigFile) |
Merge one executable module under its key() |
The single-argument typed getters return null (Kotlin Int?, Java Integer) when the key is missing or not convertible. The two-argument forms return the default instead, which is what you usually want. get performs an unchecked cast: assigning a string value to an Integer compiles but fails with ClassCastException when used, so prefer the typed getters for scalars.
Reads and writes are synchronized, so config is safe to read from request threads. Treat it as read-only after boot anyway; it is meant for settings, not shared mutable state.
You can still set keys in a configure callback without a dedicated module:
Kona.create(args) .configs(config.App, config.Database) .configure { config -> config.loadMap("feature", mapOf("beta" to true)) } .boot()Kona.create(args) .configs(new config.App(), new config.Database()) .configure(config -> config.loadMap("feature", Map.of("beta", true))) .boot();Failure modes
Section titled “Failure modes”- Missing required environment variable read with
Env.require:IllegalStateExceptionnaming the variable. - Exceptions thrown inside
ConfigFile.config()during boot propagate and fail fast before the app serves requests.
See also Application & providers for where configuration fits in the boot sequence.