Skip to content

Config Runtime

The runtime API is the day-to-day SDK experience. Configs are created via the Console UI or the management API; the runtime resolves values, subscribes to live updates, and reacts to changes.

The first runtime call triggers lazy initialization — it fetches all configs, resolves inheritance and environment overrides, and opens a WebSocket for live updates. Subsequent calls are local cache reads.

Get Resolved Values

get(key) returns a flat dict/map of resolved values for the current environment. Inheritance is walked, environment overrides applied, values unwrapped.

python
async with AsyncSmplClient(environment="production", service="my-service") as client:
    config = await client.config.get("user_service")
    print(config["database.host"])
    print(config["max_retries"])
    print(config.get("cache_ttl_seconds"))

Typed Resolution

Pass a model class or struct to get for typed access. The SDK builds a nested structure from flat dot-notation keys and constructs the model.

python
from pydantic import BaseModel

class Database(BaseModel):
    host: str
    port: int
    name: str
    pool_size: int

class UserServiceConfig(BaseModel):
    database: Database
    cache_ttl_seconds: int
    enable_signup: bool
    app_name: str = ""
    max_retries: int = 3

cfg = await client.config.get("user_service", UserServiceConfig)
print(cfg.database.host)
print(cfg.cache_ttl_seconds)
print(cfg.max_retries)

Subscribe to Live Updates

subscribe() returns a live proxy that always reflects the latest server-side state. When a WebSocket event arrives, the proxy updates automatically — no polling or re-fetching needed.

python
live = await client.config.subscribe("user_service", UserServiceConfig)
print(live.database.host)
print(live.cache_ttl_seconds)

The proxy always returns current values — after a server-side change propagates via WebSocket, subsequent reads reflect the update.

Change Listeners

Register callbacks that fire when config values change. Three scoping levels are available: global (any config), config-scoped (specific config key), and item-scoped (specific item within a config).

python
@client.config.on_change
def on_any_change(event):
    print(f"{event.config_key}.{event.item_key}: "
          f"{event.old_value!r} -> {event.new_value!r}")

@client.config.on_change("common", item_key="max_retries")
def on_retries_change(event):
    print(f"max_retries changed to {event.new_value}")

Inheritance

Configs inherit from their parent (and transitively up to common). Values defined in a child take precedence; anything not overridden falls through to the parent.

python
auth = await client.config.get("auth_module")
print(auth["session_ttl_minutes"])
print(auth["mfa_enabled"])
print(auth["app_name"])

session_ttl_minutes and mfa_enabled come from auth_module. app_name is inherited from common.

Manual Refresh

refresh() re-fetches all configs, re-resolves values, and fires change listeners for any values that differ. In production, WebSocket events trigger this automatically. Manual refresh is useful for demos, scripts, and edge cases.

python
await asyncio.sleep(2)
new_retries = (await client.config.get("user_service")).get("max_retries")

In Python, WebSocket events trigger automatic refresh. Use asyncio.sleep() to allow the event to arrive rather than calling refresh manually.

Sync Client (Python)

For synchronous applications (Django, Flask, CLI tools), use SmplClient instead of AsyncSmplClient. The API is identical but without await.

python
from smplkit import SmplClient

with SmplClient(environment="production", service="my-service") as client:
    config = client.config.get("user_service")
    host = config["database.host"]

    cfg = client.config.get("user_service", UserServiceConfig)
    print(cfg.database.host)

    live = client.config.subscribe("user_service", UserServiceConfig)
    print(live.database.host)

Next Steps