Skip to main content
Use Config Plugin (Paper/Velocity) when your plugin needs typed runtime configuration from the Grounds Config System. The plugin starts the shared config runtime, connects it to Config API and NATS, and exposes a ConfigManager that other plugins can use to register and consume typed config documents.

Requirements

The plugin requires these environment variables at startup:
VariableRequiredPurpose
CONFIG_GRPC_TARGETYesgRPC target for Config API
CONFIG_NATS_URLYesNATS connection URL used for refresh triggers
app and env are not environment variables in the plugin runtime. Consumer plugins choose them when they register each config definition.

What the Plugin Provides

After startup, the plugin:
  • creates a ConfigManager
  • connects the manager to Config API and NATS
  • creates a local snapshot cache under the plugin data directory in runtime-config-cache
  • exposes ConfigManager to other plugins
Paper exposes ConfigManager through Bukkit services. Velocity exposes it through the ConfigManagerService contract implemented by the plugin-config plugin instance.

Define a Typed Config

Create one ConfigDefinition<T> per config document. The definition declares the backend namespace, the key, the Kotlin type, and the local default value.
data class LobbySettings(
    val maxPlayers: Int = 20,
    val motd: String = "Welcome to the lobby!",
    val allowedGameModes: List<String> = listOf("survival", "adventure"),
)

object LobbySettingsConfig :
    ConfigDefinition<LobbySettings>(
        namespace = "lobby",
        key = "settings",
        type = LobbySettings::class.java,
        defaultValue = LobbySettings(),
    )
The default value is serialized to JSON during bootstrap and sent to Config API through SyncDefaults.

Integrate the Manager

Resolve ConfigManager from Bukkit services:
val configManager =
    server.servicesManager.load(ConfigManager::class.java)
        ?: error("ConfigManager service not available — is plugin-config installed?")

Register, Read, and Observe Config

1

Register your definitions

Register each definition for the (app, env) scope it belongs to.
val lobbyRegistration =
    configManager.register(
        LobbySettingsConfig,
        app = "lobby",
        env = "prod",
        startupMode = ConfigStartupMode.FAIL_CLOSED,
    )
The first registration for a given (app, env) scope creates the internal sync scope for that pair.
2

Choose the startup mode

Use FAIL_CLOSED when the plugin cannot continue without a ready value. Use DEGRADED when the plugin may continue with a cached snapshot if bootstrap cannot fetch a current one.
  • FAIL_CLOSED: throws ConfigRegistrationException if bootstrap cannot produce a ready value
  • DEGRADED: restores a persisted cached snapshot when possible, otherwise returns NOT_READY
Inspect the returned ConfigRegistrationResult when you allow degraded startup.
if (!lobbyRegistration.isUsable()) {
    logger.warning(
        "Lobby config not ready at startup " +
            "(status=${lobbyRegistration.status}, reason=${lobbyRegistration.reason})"
    )
}
3

Read the current value

Once a definition is registered and ready, read it through indexed access on ConfigManager.
val lobbySettings = configManager[LobbySettingsConfig]
logger.info(
    "Lobby settings loaded (maxPlayers=${lobbySettings.maxPlayers}, motd=${lobbySettings.motd})"
)
4

Subscribe to updates

Register change handlers for live refresh:
configManager.onChange(LobbySettingsConfig) { settings ->
    logger.info(
        "Lobby settings changed (maxPlayers=${settings.maxPlayers}, motd=${settings.motd})"
    )
}
The callback receives the updated typed value after reconciliation applies the new snapshot.

Registration Outcomes

ConfigManager.register(...) returns a ConfigRegistrationResult with a status:
StatusMeaning
READYA typed value is available immediately
DEGRADEDA cached snapshot was restored and the value is usable
NOT_READYRegistration succeeded, but no typed value is available yet
ALREADY_REGISTEREDThe same definition was already registered
REJECTEDAnother definition already owns the same config key

Failure Modes

Reading or subscribing to a definition that was never registered throws ConfigDefinitionNotRegisteredException.
Reading a registered definition before a typed value exists throws ConfigDefinitionNotReadyException.
If a second definition tries to claim the same backend key in the same scope, registration is rejected with status REJECTED.

Local Cache Behavior

The runtime stores cached snapshots in runtime-config-cache under the plugin data directory:
  • Paper: inside the plugin-config data folder
  • Velocity: inside the plugin-config data directory
The cache exists to support DEGRADED startup when the current snapshot cannot be fetched during bootstrap.
Cached snapshots improve startup resilience. They do not replace reconciliation through the current service snapshot.

Next Steps