Skip to content

LiveConfig

LiveConfig is feature flags and tunables changed without redeploying. Values live in a single polled MemoryStore document; set() from any server reaches the whole fleet within one poll interval, and set(key, nil) deletes the override so every server reverts to its default. The defining design decision: defaults ship in code, overrides live in memory. A server that cannot reach MemoryStore runs on its compiled-in defaults — LiveConfig degrades to a static config table, never to missing values.

Each instance holds three tables:

  • Defaults — a clone of what you passed at construction. Never mutated.
  • Values — the effective table get() reads. Starts as a clone of Defaults.
  • Overrides — a set marking which keys currently differ because the document says so.

The remote side is one document under the key "config" in a MemoryDriver — by default a MemoryStore hash map named CKLiveConfig with a 7-day TTL (7 * 24 * 3600 seconds). The whole fleet polls the same document.

The poll loop (every PollSeconds, default 30, at Background priority) fetches the document and reconciles:

  1. Every key in the document becomes an override. If the effective value actually changed, Config.Changed publishes on the bus.
  2. Every key currently overridden but absent from the document reverts to its default — otherwise a deleted override would stay stale forever. The revert publishes Config.Changed too.

A boot-time poll runs immediately at construction (in a spawned task, so new() never yields) — without it, overrides would apply one full interval late on fresh servers.

Change detection is by value, not by write. Scalars compare with ==; tables compare by their Serde encoding, so a poll that re-reads an identical table does not re-fire Config.Changed. Subscribers only hear about effective changes.

set() is write-through. It runs a retried read-modify-write (UpdateAsync underneath) on the document, then applies the change locally at once — the server issuing the change sees it immediately, the rest of the fleet within one poll. If the MemoryStore write fails after retries, set returns (false, err) and nothing changes anywhere.

-- src/Server/Bootstrap.luau
local Root = game:GetService("ServerScriptService").ChloeKernelServer
local LiveConfig = require(Root.LiveConfig)
return function(kernel)
local Config = LiveConfig.new(kernel, {
Defaults = {
BossHealth = 100000,
DoubleXp = false,
NewShopEnabled = true,
},
})
-- read anywhere, any time — always answers, override or default
local Health = Config:get("BossHealth")
-- react to changes (local set or remote poll)
kernel.Bus:subscribe("Config.Changed", function(_, key, value)
if key == "DoubleXp" then
announceEvent(value)
end
end)
end

set() writes the shared document, so it works from any server — including a fresh instance constructed in the live dev console. Every instance pointing at the same store name converges on the same document:

-- in the developer console of any live server:
local Root = game:GetService("ServerScriptService").ChloeKernelServer
local LiveConfig = require(Root.LiveConfig)
local ServerKernel = require(Root)
local Config = LiveConfig.new(ServerKernel.current(), {
Defaults = {}, -- ops instance: no local reads needed
SkipLoop = true, -- no poll loop; this instance only writes
})
Config:set("DoubleXp", true) -- whole fleet within one poll (≤30s)
Config:set("NewShopEnabled", false) -- mid-incident kill switch
Config:set("DoubleXp", nil) -- delete the override: fleet reverts to defaults

The ops loop this enables: flip a flag during an incident, watch it land within a poll, and once the dust settles ship the new value as a code default and delete the override.

Member Description
LiveConfig.new(kernel, options) → LiveConfig Clones Defaults into the effective table, builds the MemoryStore driver, and (unless SkipLoop) starts the poll loop plus an immediate boot-time poll
config:get(key) → any The effective value: the override if the document has one, else the default. Pure table read — never yields
config:set(key, value) → (boolean, string?) Read-modify-writes the shared document, then applies locally. set(key, nil) deletes the override; every server reverts to its default on its next poll. (false, err) when the store write failed
Option Default Meaning
Defaults required The compiled-in value per key — what every server runs on when no override exists or MemoryStore is unreachable
PollSeconds 30 Poll cadence; also the fleet-wide propagation bound
Driver MemoryDriver on hash map CKLiveConfig, 7-day TTL Injected store — mocks in specs, or a differently-named store to run several independent config namespaces
SkipLoop false No poll loop and no boot-time poll — write-only ops instances and specs
Topic Args Published when
Config.Changed key, value The effective value of a key changed — local set, a remote override arriving via poll, or a revert to default (then value is the default). Never fires for writes that leave the value equal
  • Unreachable store = defaults, not errors. A failed poll changes nothing: get() keeps serving whatever the server last knew (defaults on a fresh server). Only set() surfaces store failures, through its return values.
  • Convergence is bounded, not instant. Servers poll on independent clocks, so a fleet-wide change lands over a window of up to PollSeconds. During that window Config.Changed fires at different times on different servers — do not use it to coordinate anything that needs simultaneity.
  • Keys outside Defaults work, with one asymmetry. A set on an undeclared key propagates and get serves it — but its “default” is nil, so deleting the override reverts it to nil. Declare every key you intend to use.
  • One document, 32KB. The entire config table serializes into a single MemoryStore value, which caps at 32KB. LiveConfig is for flags and tunables, not content tables — dozens of scalars, not item catalogs.
  • Polls cost quota. Every server reads one MemoryStore value every PollSeconds. At the default 30s that is well inside quotas at any realistic server count; lowering PollSeconds for faster propagation trades directly against them.