---
title: "Change config without a deploy"
description: "Store runtime settings and flags, read them with config.get, watch changes, and roll a feature out in steps with no deploy."
section: "guides"
status: "Building for 0.2"
spec: "config-secrets"
canonical: "https://mainsdk.dev/docs/guides/change-config-without-a-deploy"
updated: "2026-10-28"
---

# 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: Building for 0.2 · Canonical: https://mainsdk.dev/docs/guides/change-config-without-a-deploy · Updated: 2026-10-28

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.

> **Note:** `config` is being built for 0.2 and `flags` for 0.3 (specs `config-secrets` and `flags`). Both run from the repository and are not published to npm yet.

## You need

- A MainSDK app with a `main.yaml` (see [Thirty-minute path](https://mainsdk.dev/docs/getting-started/thirty-minutes)).
- The settings you want to change, written down with their types.
- For production, a store that several instances share: the `redis` driver. The `files` driver 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.

```json title="config.schema.json"
{
  "type": "object",
  "properties": {
    "cart": {
      "type": "object",
      "properties": { "maxItems": { "type": "integer" } }
    }
  }
}
```

```yaml title="main.yaml"
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

```ts title="app/api/cart/route.ts"
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

```ts title="server/worker.ts"
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:

```json title="flags.<name> in the store"
{ "enabled": true, "rollout": 5 }
```

```ts title="app/api/checkout/route.ts"
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](https://mainsdk.dev/docs/interfaces/config#pushing-mainyaml-values).

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](https://mainsdk.dev/docs/interfaces/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:

```bash
npx mainsdk check --env prod
```

A 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

```text
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 `openfeature` flag driver, so definitions can stay in LaunchDarkly, Flagsmith or PostHog (planned).

## Next

- [Runtime settings](https://mainsdk.dev/docs/interfaces/config): the full reference.
- [Feature flags](https://mainsdk.dev/docs/interfaces/flags): definitions, rules and rollout.
- [Secrets](https://mainsdk.dev/docs/interfaces/secrets): credentials, server only.
