Skip to content

Smpl Config overview

Smpl Config holds your application's configuration values — connection strings, timeouts, feature toggles for things that aren't behavioral flags. Configs support parent-based inheritance so a service can pull its values from a shared "common" parent and override only what differs, and environment overrides so production gets different values than staging without forking the config.

A concrete example

You have three services that all need the same Postgres host and port. Instead of duplicating those values across three configs, you put them in a shared parent and let each service inherit:

common (parent)
  database.host = "db-prod.acme.internal"
  database.port = 5432

  payments (child of common)
    database.pool_size = 20

  notifications (child of common)
    database.pool_size = 5

When payments resolves its config, it sees database.host, database.port, and database.pool_size. The first two come from common; the third is its own. Change database.host in common and all three services pick it up — over WebSocket, immediately.

Configs and items

A config is a named bundle of items belonging to one account. It carries:

  • key — the stable identifier in URLs and SDK calls (payments, user-service, common).
  • name — display label.
  • description — free text.
  • parent — UUID of another config to inherit from (or null for standalone).
  • items — flat map of dot-notation keys to typed values.
  • environments — per-environment overrides on the same items.

An item is one entry in items:

json
{
  "database.host": {
    "value": "db-prod.acme.internal",
    "type": "STRING",
    "description": "Primary database host."
  }
}

Item types: STRING, NUMBER, BOOLEAN, JSON. JSON values are stored and replaced wholesale — never deep-merged.

Flat dot-notation keys

Items are a flat map keyed by dot-notation strings. The dots are convention only — the platform doesn't interpret them as a hierarchy. database and database.host are independent keys that can coexist.

When the SDK resolves a config to a Python dict, Pydantic model, or JSON object, it expands dot-notation keys back into nested objects:

python
items = {
    "database.host": {"value": "localhost", "type": "STRING"},
    "database.port": {"value": 5432, "type": "NUMBER"},
}

# resolves to:
{"database": {"host": "localhost", "port": 5432}}

Inheritance

Each config has an optional parent pointing to another config in the same account. Children inherit all keys, types, and descriptions from their ancestor chain. Override at any level by specifying the same key with a different value — type and description always come from the defining config.

A config inherits only from its explicit parent. If parent is null, the config stands alone — it doesn't fall back to any default.

The depth of an inheritance chain is capped:

  • Hard cap: 10 levels. Including the config itself. Writes that would exceed this return 400.
  • Plan-driven cap: config.inheritance_depth. Lower limit on free plans, higher on paid.

Circular references are rejected at write time — setting a parent that would create a cycle returns 400.

The common config

Every account has a built-in config with key common. It's auto-created lazily the first time someone calls GET /api/v1/configs (i.e. lists configs). After creation it's a normal config: editable name, key, description, items, environments.

common serves two roles:

  1. Default parent. New configs without an explicit parent field default to common.
  2. Cross-config defaults. Items defined here are inherited by every config that has common (directly or transitively) in its ancestor chain.

common cannot be deleted — DELETE /api/v1/configs/common returns an error.

Note

Because common is created lazily, a freshly-provisioned account that never lists configs won't have a common row yet. The first list call (or first config created in the console) triggers creation.

Environment overrides

Per-environment overrides are stored under environments[env][item_key] as a flat map from item key to the override's raw value:

json
{
  "items": {
    "database.host": {"value": "localhost", "type": "STRING"}
  },
  "environments": {
    "production": {
      "database.host": "db-prod.acme.internal"
    }
  }
}

An override carries only the value — the type and description always come from the defining config in the inheritance chain. Because the override has nothing else to carry, it's a bare key: value pair rather than a {value: V} wrapper.

Resolution for a key in environment E:

  1. Check config.environments[E][key] — return if found.
  2. Check config.items[key] — return if found.
  3. If config.parent is set, restart at step 1 with the parent.
  4. Repeat up the chain.
  5. If never found, return null.

There's no merge anywhere. The first matching value at the lowest level wins. There are no "tombstones" — you can't remove an inherited key, only override it with a different value.

Discovered vs managed — not yet

Config does not currently have a discovered/managed split. There's no managed column, no POST /api/v1/configs/bulk endpoint, no auto-registration from the SDK. Every config is explicitly created (in the console or via the management API), and every config counts toward config.items.

Flags and Logging both have a discovered/managed model where SDK-registered resources don't consume entitlement until promoted. The same model isn't yet implemented for Config — see Discovered vs managed.

Promote to managed

Not yet supported

Smpl Config does not currently have a promote-to-managed mechanic — every config is created managed from the start. The mechanics described under Discovered vs managed (auto-discovery via the SDK, deferred entitlement counting, explicit promote action) apply to Smpl Flags and Smpl Logging today, but not to Config. When parity ships, the section below will explain the promotion flow.

What works today is explicit creation, configuration, and deletion of configs:

  • Create via the Config page in the console.
  • Create via the API: POST /api/v1/configs with id and any initial items.
  • Create via the SDK: manage.config.new(...) then .save().

Every config you create counts immediately toward config.items. The closest equivalent to "demoting" is deleting the config to free a slot.

For the full step-by-step API and SDK examples, see Smpl Config Management.

Limits

Plan-driven (entitlement keys):

  • config.items — total configs per account.
  • config.keys — items per config (validated per write).
  • config.value_size_bytes — byte size of each item value.
  • config.inheritance_depth — chain depth.

Plus the hard caps:

  • Inheritance depth: 10 levels — absolute, regardless of plan.

API surface

  • GET /api/v1/configs — list; lazily creates common on first call
  • GET /api/v1/configs/{key} — fetch one
  • POST /api/v1/configs — create (id required in body; defaults parent to common)
  • PUT /api/v1/configs/{key} — full replace; can rename via id field
  • DELETE /api/v1/configs/{key} — soft-delete; returns 409 if other configs reference as parent