Skip to content

Kairo Configs

Application configuration should be simple, consistent, and powerful. kairo-config standardizes on lightbend/config’s HOCON implementation, giving every Kairo project a familiar, robust foundation for managing configs.

production.conf
include "common.conf" # Include another config file.
sql.connectionFactory {
url = ${?POSTGRES_URL} # Environment variable.
username = "kairo_sample"
password = "gcp::projects/012345678900/secrets/example/versions/1" # Config resolver.
ssl = false
}
  • Rich features

    • Built-in config inheritance and overrides for easy multi-env (dev/staging/prod).
    • Native environment variable substitution.
  • Developer ergonomics: Comments, human-readable syntax, less boilerplate.

Kairo’s config resolvers let you pull in properties from other sources like Google Secret Manager.

Install kairo-config. You don’t need to install lightbend/config separately — it’s included by default.

build.gradle.kts
dependencies {
implementation("software.airborne.kairo:kairo-config")
}

lightbend/config’s documentation explains HOCON and config management in detail. Here’s how to use it through Kairo.

First, define your config class using a data class.

data class Config(
val rest: RestFeatureConfig,
val sql: SqlFeatureConfig,
)

You’ll probably have a base config with settings shared across environments.

common.conf
rest {
connector.port = 8080
plugins.defaultHeaders.serverName = "Kairo Sample"
}

Your production config will extend the base config, specifying anything that was missing and/or overriding things that are different.

production.conf
include "common.conf"
sql.connectionFactory {
url = ${?POSTGRES_URL} # Environment variable.
username = "kairo_sample"
password = ${?POSTGRES_PASSWORD} # Environment variable.
ssl = false
}

You can create a staging config the same way.

Your development config will also extend the base config, but override a few settings.

development.conf
include "common.conf"
rest {
parallelism { runningLimit = 10, callGroupSize = 2 } # Reduced parallelism.
plugins.callLogging.useColors = true # Nice local experience.
}
sql {
connectionFactory {
url = "r2dbc:postgresql://localhost/kairo_sample"
username = ${?KAIRO_SAMPLE_POSTGRES_USERNAME}
password = ${?KAIRO_SAMPLE_POSTGRES_PASSWORD}
ssl = false
}
connectionPool.size { initial = 2, min = 1, max = 5 } # Reduced pool size.
}

Back to your Kotlin code — load the config and deserialize it into your config class.

val config: Config = loadConfig<Config>(configName("production"))

If you want to dynamically choose which config to load, omit configName and set the CONFIG environment variable instead.

val config: Config = loadConfig<Config>()

Config resolvers let you pull in properties from other sources like Google Secret Manager. To use config resolvers, pass them to loadConfig.

private val gcpSecretSupplier: GcpSecretSupplier = DefaultGcpSecretSupplier()
val config: Config =
loadConfig(
resolvers = listOf(
ConfigResolver("gcp::") { gcpSecretSupplier[it]?.value },
)
)

This will detect any strings (or ProtectedStrings) that start with gcp::, resolving them using the GcpSecretSupplier.

Now you could change your production config to pull GCP secrets.

production.conf
include "common.conf"
sql.connectionFactory {
url = ${?POSTGRES_URL}
username = "kairo_sample"
password = "gcp::projects/012345678900/secrets/example/versions/1" # Config resolver.
ssl = false
}