Change config without a deploy
Store runtime settings and flags, read them with config.get, watch changes, and roll a feature out in steps with no deploy.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Change a limit, a banner or a rollout while the app runs. You will type your settings, read them with config.get, react to changes with config.watch, and roll a feature out to a growing share of users with a flag. It takes about 20 minutes.
You need
- A MainSDK app with a
main.yaml(see Thirty-minute path). - The settings you want to change, written down with their types.
- For production, a store that several instances share: the
redisdriver. Thefilesdriver reads JSON from your repository and suits development.
1. Type the settings
Describe each setting in a JSON schema. A value that doesn't match it is refused, and the last good value stays in place.
{
"type": "object",
"properties": {
"cart": {
"type": "object",
"properties": { "maxItems": { "type": "integer" } }
}
}
}config:
driver: redis
schema: ./config.schema.json
redis: { url: secret:REDIS_URL }Use driver: files in main.env.dev.yaml to keep development on the repository.
2. Read values with config.get
import { config } from "mainsdk";
export function canAdd(count: number) {
return count < (config.get("cart.maxItems") as number); // synchronous, from memory
}config.get() reads memory, so call it inside the request. Don't read main.yaml or the store at run time; the value you need is already there.
3. React to a change with config.watch
import { config, log } from "mainsdk";
let batchSize = config.get("worker.batchSize");
const stop = config.watch("worker.batchSize", (value, change) => {
batchSize = value;
log.info("batch size changed", { from: change.previous, to: value });
});
process.on("SIGTERM", () => stop());The watcher runs once for each change, after it is applied, outside any request. A watcher that throws is logged and does not stop the others.
4. Roll a feature out in steps
A flag is a definition in the same store, at flags.<name>. Start small and raise rollout as you gain confidence:
{ "enabled": true, "rollout": 5 }import { flags } from "mainsdk";
export function checkoutFor(userId: string) {
return flags.enabled("new-checkout", { user: userId }) ? "new" : "classic"; // no await
}Move rollout to 25, then 100. Each step applies without a restart. A user who was on stays on, because each user has a fixed place in the rollout. To give a different group of users the early access, change the salt.
To turn it off at once, set enabled to false. That wins over every rule.
5. Know what a push can change
A push from the store can only tighten main.yaml. A spend limit can go down, network.mode can go up to enforce, and a host can be removed. It can't add a host, raise a limit or switch a rule off. A push that loosens a rule is refused with MAIN-CONFIG-0021, and a main. path that only changes in a reviewed deploy, such as main.payments.driver, with MAIN-CONFIG-0022. The table of pushable paths is in Runtime settings.
The rule keeps a compromised store from loosening the app. The reason is in the decision: a loosening is a deploy, so a person reviews it.
Keep secrets out of the store. A literal key under a secret field, such as apiKey, fails with MAIN-CONFIG-0004. Put it in Secrets and refer to it with secret:NAME.
6. Check it works
In a test, use the memory driver. Its handle sets a value, the watcher fires, and config.get() returns the new value at once. Then check the app:
npx mainsdk check --env prodA pass exits 0. To prove the app survives an outage, make the store unreachable in a test: config.get() keeps the last good value and MAIN-CONFIG-0023 is logged once.
If it fails
| Code | Cause | Fix |
|---|---|---|
MAIN-CONFIG-0004 | A literal secret sits in a config value, or config.redis.url is a literal | Use a secret: reference and keep the secret in secrets |
MAIN-CONFIG-0020 | A value fails the schema, or a file isn't valid JSON | Correct the value; the last good one stays |
MAIN-CONFIG-0021 | A pushed main. value is looser than main.yaml | Push a tighter value, or change main.yaml in a reviewed deploy |
MAIN-CONFIG-0022 | A pushed main. path only changes in a reviewed deploy | Change it in main.yaml and deploy |
MAIN-CONFIG-0023 | The store can't be read; the app serves its last good values | Restore the store; no restart is needed |
MAIN-FLAGS-0001 | A flag was checked with no definition; the fallback was served | Define flags.<name>, or check the name |
MAIN-FLAGS-0002 | A flag definition is invalid and was refused | Fix enabled, rollout (0 to 100, two decimals at most) or the rule keys |
MAIN-FLAGS-0003 | A rule's value has another type from the fallback | Serve the same type as the fallback |
For agents
Goal: change a runtime setting or roll out a feature without a deploy.
Read: docs/specs/config-secrets.md, docs/specs/flags.md, this page, main.yaml, config.schema.json.
Touch only: config.schema.json, the code that reads the setting or the flag, and tests.
Do: read values with config.get and flags.enabled, and never await them; never re-read main.yaml at run time;
keep secrets in secrets; ask the owner before raising a limit or widening network.allow in main.yaml.
Verify: npx mainsdk check --json --agent
Never write a push that loosens main.yaml; the store can only tighten it.What comes later
- A console to edit values, staged rollout that advances by itself, and rollback on error spikes: Cloud (ADR-0007).
- The
openfeatureflag driver, so definitions can stay in LaunchDarkly, Flagsmith or PostHog (planned).
Next
- Runtime settings: the full reference.
- Feature flags: definitions, rules and rollout.
- Secrets: credentials, server only.
Was this page helpful?