KDCube
← Engineering
KDCube Engineering · Deep Dive

KDCube Application Configuration: Six Data Classes, One Live Contract

Configuration is not one YAML document. Six data classes keep ownership, secrecy, scope, and export rules explicit.

18 July 2026Engineering17 minExperienceEngine Room
app configurationpropertiessecretsruntime contextKDCube bundles.yamllive app configurationuser-scoped secrets

Configuration is not one YAML document. KDCube separates deployment properties, deployment secrets, user properties, user secrets, platform settings, and platform secrets because they have different owners and export rules. App code reads one effective contract through supported SDK helpers; provider-backed reads and writes remain async. It does not parse environment variables or reach behind the configured authorities.

An AI app configuration model has to answer more than "where is the key?" It must answer who owns the value, whether it is secret, whether it belongs to one user or the whole deployment, which runtime may receive it, when a change takes effect, and whether it can be exported.

KDCube makes those dimensions explicit. Code defaults describe the app's portable non-secret shape. bundles.yaml carries deployment overrides and surface policy. bundles.secrets.yaml or another configured provider carries deployment app secrets. User properties and user secrets remain in user-scoped stores. A small request-scoped context can cross supported runtime boundaries without becoming durable configuration.

This article explains that configuration engine. The package and first-surface story remains in Build an AI App with KDCube; the exact change/reload moments are treated separately in the application lifecycle article.

KDCube now says app in builder-facing prose. Literal platform contracts still say bundle in names such as bundles.yaml, bundle_id, bundle_prop(...), and kdcube bundle config apply.

00Implementation highlights

ConceptThe takeaway
Three scopes, six data classesPlatform, deployment app, and user app scope each have separate non-secret and secret state.
Effective properties are a mergeCode defaults are deep-merged with descriptor/admin overrides; app code reads the effective result.
Secrets do not join that mergeSecret values have no code defaults, remain in the configured provider, and are resolved asynchronously.
Redis is a cache, not descriptor authorityLive app-property writes persist to authority first, then update Redis and notify workers.
Request context is not configurationbundle_call_context carries small JSON-safe invocation facts but is neither durable nor exportable.
Platform-reserved keys stay app propertiesenabled, surfaces, model roles, UI, execution, and public-content blocks are ordinary app properties with platform interpretation.

01The scope decision comes first

KDCube has three configuration scopes:

platform / deployment
  one tenant/project runtime

app deployment
  one app for that tenant/project

app user
  one resolved user inside that app

Each scope has non-secret and secret state, giving six data classes:

Data classTypical examplesRead contractDurable owner
Platform propertiesauth mode, service ports, storage backend selectiontyped platform settingspromoted platform configuration
Platform secretsdeployment-wide provider key, signing secretawait get_secret("canonical.key")configured secrets provider
App propertiesfeature gate, model role, endpoint, cron, surface policyself.bundle_prop(...)app descriptor authority, with Redis runtime cache
App secretsapp-specific provider key, webhook secret, MCP credentialawait get_secret("b:...")configured secrets provider
User app propertiestheme, selected account, product preferenceasync user-property helpersPostgreSQL user_bundle_props
User app secretsone user's personal provider credentialawait get_secret("u:...")configured secrets provider under user scope

The classification prevents common leaks:

  • a Slack access token cannot become a non-secret app property;
  • one user's selected account cannot become an app-wide default;
  • a deployment signing key cannot be written by ordinary app code;
  • a model-role choice for one invocation does not have to mutate durable app configuration.

App code may read all six through supported SDK contracts. It normally writes only its own deployment properties/secrets and its own user properties/secrets. Platform/global state remains operator-owned.

One app entry, three different contracts

The app item in bundles.yaml contains both registry facts and non-secret app properties, but they do not have the same runtime meaning:

descriptorYAML
bundles:
  items:
    - id: reporting@1-0

      # Registry/source and lifecycle contract
      repo: https://github.com/example/acme-apps.git
      ref: 2026.07.18.1
      subdir: apps/reporting
      module: entrypoint
      singleton: true

      # Non-secret effective app-property overrides
      config:
        reports:
          page_size: 50
        enabled:
          cron:
            refresh-index: true

The registry fields tell proc which code generation to resolve and how to load it. They do not participate in self.bundle_prop(...). The config mapping does participate in the effective-property merge described below. App secrets live under the matching app id in bundles.secrets.yaml or its configured provider; they never become another block in config.

That separation also explains the change boundaries. Editing reports.page_size is a property update. Selecting a new ref, subdir, module, or singleton setting changes the loaded app definition and requires generation invalidation. Rotating a secret changes the secret authority and the clients that resolve from it. One editor may present these controls together, but the runtime does not collapse their semantics.

Six configuration data classes across three scopes An Engine Room control panel separates platform, app deployment, and app user scope. Each scope has an independent properties channel and secrets channel with its own read contract and authority. CONFIGURATION CONTROL PANEL Three scopes. Two classes in every scope. PLATFORM 01 Properties get_settings() promoted platform config Secrets await get_secret("key") configured secrets provider OWNER tenant + project deployment APP DEPLOYMENT 02 Properties self.bundle_prop(...) descriptor authority + cache Secrets await get_secret("b:...") configured secrets provider OWNER tenant + project + app APP USER 03 Properties await get_user_prop(...) PostgreSQL user_bundle_props Secrets await get_secret("u:...") provider under user scope OWNER tenant + project + app + user RULE Scope chooses the owner. Property vs secret chooses the authority and API. No merge or implicit fallback crosses a brass secret boundary.
Engine Room control panel with three vertical bays: Platform, App Deployment, App User. Each bay has a cool "properties" gauge and a locked brass "secrets" gauge. Under the panel, arrows show the read API and durable owner. No arrow crosses between property and secret gauges.

02Code defaults plus deployment overrides produce effective properties

Stable, non-secret defaults intrinsic to the app belong in configuration_defaults() or the entrypoint's configuration default contract. Deployment-specific overrides belong under the app item in bundles.yaml.

app code
  configuration_defaults()
        |
        | recursive dictionary merge
        v
effective app properties
  self.bundle_prop("path.to.value")
        ^
        |
bundles.yaml / Bundle Admin authority
  bundles.items[].config

Merge behavior is intentionally simple:

  • dictionaries merge recursively;
  • a descriptor/admin value wins at the same path;
  • lists and scalar values replace the default at that path;
  • an absent override leaves the code default effective.

For example, code can declare a safe default:

SDK examplePYTHON
def configuration_defaults(self):
    return {
        "reports": {"page_size": 25, "export_enabled": False},
        "enabled": {"cron": {"refresh-index": True}},
    }

The deployment can override only what differs:

descriptorYAML
bundles:
  items:
    - id: reporting@1-0
      config:
        reports:
          export_enabled: true

App code reads:

SDK examplePYTHON
page_size = self.bundle_prop("reports.page_size", 25)
export_enabled = self.bundle_prop("reports.export_enabled", False)

Code defaults are not copied into bundles.yaml merely because the app loaded. Bundle Admin can show persisted props and code defaults separately. An explicit reset/materialization action can write defaults when that is the operator's intent, but the normal descriptor remains a concise set of deployment choices.

03App properties are the operator-visible configuration channel

Every app knob an operator may need to change belongs in the property contract:

  • timeouts and limits;
  • feature switches;
  • model-role and embedding selection;
  • surface enablement and visibility;
  • MCP server endpoints and per-agent inventory;
  • UI source/build declarations and product presentation;
  • cron schedules and job policy;
  • public-content catalogs;
  • app-specific storage/index behavior.

An environment variable read inside app code is not an app configuration surface. It is invisible to the app descriptor, Bundle Admin, config export, and the normal live-update path. Platform processes may use deployment environment internally to assemble platform settings, but app behavior must be declared as app properties.

When an existing standalone solution uses environment variables, keep that idiom inside the standalone package and overlay KDCube app properties at the adapter boundary. In the hosted app, the descriptor value is authoritative.

Module-level globals are not a replacement either. They are process-local, cannot represent per-user or per-invocation state, survive changes unpredictably across imports, and disappear on worker restart.

04Reserved properties are still part of the same app contract

Some property paths are interpreted by platform subsystems. They are not a fourth store and not hidden metadata.

Property familyPlatform meaning
enabled.*app and per-surface feature-gate overrides
surfaces.as_provider.*visibility, authentication ownership, and product intent for offered surfaces
surfaces.as_consumer.*outbound connections, per-agent tool inventories, UI/scene consumption
role_modelsmodel routing by semantic role
embeddingembedding provider/model selection
execution.runtimeapp-level generated-code runtime and limits
ui.widgets.*, ui.main_view.*UI source/build and site configuration
public_content.*public-content enablement, canonical base, catalogs, and presentation
economics.*app-level economics defaults interpreted by guarded surfaces

Surface enablement is an override layer, not a duplicated inventory:

descriptorYAML
config:
  enabled:
    bundle: true
    api:
      "operations.export.POST": false
    mcp:
      reports: true
    widget:
      reporting: true
    cron:
      refresh-index: false

Decorators and code defaults declare what exists and its normal enabled state. The descriptor should carry intentional deployment differences, not mirror every surface as true.

enabled answers whether a surface is active. surfaces.as_provider answers who may reach it and which authentication owner applies. Combining those into one flag would lose the distinction between existence and policy.

05Secrets have no defaults and never enter the property merge

Deployment app secrets live in bundles.secrets.yaml in local secrets-file mode or in the configured secrets provider in another deployment mode.

descriptorYAML
# bundles.secrets.yaml
bundles:
  items:
    - id: reporting@1-0
      secrets:
        integrations:
          partner:
            api_key: replace-through-secret-provider

App code resolves them asynchronously:

SDK examplePYTHON
from kdcube_ai_app.apps.chat.sdk.config import get_secret

api_key = await get_secret("b:integrations.partner.api_key")

The b: prefix means the current app. The u: prefix means the current resolved user inside the current app. A canonical unprefixed key addresses platform/global secret scope.

App code can apply a useful service-key override sequence explicitly:

await get_secret("b:services.<provider>.<key>")
  -> platform/global secret services.<provider>.<key>

This lets one app use its own model, Git, payment, or search credential while other apps inherit the deployment default. The fallback is an async app choice, for example:

SDK examplePYTHON
api_key = (
    await get_secret("b:services.openai.api_key")
    or await get_secret("services.openai.api_key")
)

The supported configuration path does not place secret values in self.bundle_props or bundles.yaml. Trusted app code must also avoid copying resolved secrets into:

  • model prompts or generated code;
  • browser config;
  • logs;
  • bundle_call_context.

Async secret helpers keep provider I/O off the shared event loop. A long-lived client that captured a secret at construction still needs an explicit reconstruction policy after rotation; secret storage does not mutate an already-created client object.

06User properties and user secrets are separate from app deployment state

User-scoped app properties are durable product state in PostgreSQL. User-scoped secrets remain in the configured secret provider. Neither is merged into deployment app properties or exported to app descriptors.

tenant + project + app + resolved user + typed key

The resolved user may be a platform account in chat or a stable app-owned external identity on a public integration. App code should not assume every user scope has a browser login.

Use user properties for decisions that must survive requests:

  • selected account or workspace;
  • notification preferences;
  • UI/product choices;
  • per-user feature settings;
  • remembered configuration that is not secret.

Use user secrets for personal credential material:

  • provider access/refresh tokens;
  • app passwords;
  • user-specific API keys.

The helper names are intentionally uniform, and app clients use them as async APIs: await get_user_prop(...), await set_user_prop(...), await delete_user_prop(...), and the corresponding user-secret helpers. Direct SQL or provider-internal access would bypass the app/user scope and future backend evolution.

07The portable context room is not a seventh store

Some values belong only to the current invocation but must follow nested work across supported runtime boundaries. KDCube carries those JSON-safe facts in bundle_call_context.

Examples:

  • request mode such as preview/final;
  • app-owned correlation metadata;
  • a temporary model-role overlay;
  • small inputs a background or fenced runtime must reconstruct.
current API/chat/job invocation
  -> bundle_call_context
  -> nested async task / tool / supported child runtime
  -> restored for that execution

This context is not durable, not exported, and not a secret channel. If the value must affect a later independent request, persist it first as an app property, user property, job payload, or app-owned domain record.

The context carries descriptors and ids, not live database pools, Redis clients, callbacks, or file bytes. Trusted services are reconstructed on the other side. In the split generated-code runtime, privileged descriptor and credential material stays with the trusted supervisor rather than the restricted executor.

08The live property path has one authority

For deployment app properties, the normal write journey is:

Bundle Admin or await set_bundle_prop(...)
  -> load and validate current app descriptor authority
  -> persist the change to that authority
  -> update Redis app-property cache
  -> publish bundles.props.update
  -> cached instances refresh effective properties
  -> on_props_changed only when effective values differ

Redis accelerates runtime reads and distributes change notification. It is not the durable authority. After cache loss, the descriptor authority rebuilds the runtime view.

Authority depends on deployment mode:

ModeApp property authorityApp secret authority
CLI local composestaged writable bundles.yamlbundles.secrets.yaml when secrets-file mode is active, otherwise configured provider
Direct local serviceexplicitly addressed descriptor fileconfigured provider/file
Cloudconfigured app descriptor authority; recommended file-backed ECS shape uses writable mounted descriptor stateconfigured secrets provider such as AWS Secrets Manager

Code should not branch on those backends. It uses bundle_prop, secret, and user-state helpers.

Live application property authority and cache loop Bundle Admin or the asynchronous set_bundle_prop helper persists to descriptor authority first. Redis caches descriptor properties and publishes an update. App instances merge code defaults with the refreshed properties, then run on_props_changed when the effective result changed. LIVE PROPERTY CIRCUIT Authority first. Cache second. Effective props at the instance. WRITE SOURCES Bundle Admin await set_bundle_prop() 01 · DURABLE Descriptor authority load · validate · persist bundles.items[].config 02 · RUNTIME Redis props cache descriptor/admin props CACHE · NOT AUTHORITY 03 · WORKER App instance refresh_bundle_props() worker-local generation persist cache refresh 04 · RESOLVE code defaults configuration_defaults() + descriptor props recursive deep merge 05 · APPLY Effective props self.bundle_prop() on_props_changed() publish bundles.props.update A cache loss rebuilds from authority. A notification accelerates convergence; it does not become the record.
Dark Engine Room loop: Descriptor Authority -> Redis Cache -> App Instance -> Effective Props. Bundle Admin and set_bundle_prop enter at Authority. A notification wire from Redis triggers refresh/on_props_changed. A brass warning plate beside Redis reads "cache, not authority."

09Apply and export preserve the ownership split

KDCube keeps app descriptors portable without pretending every runtime value belongs in them.

seed descriptor set
  bundles.yaml
  bundles.secrets.yaml
      |
      | kdcube bundle config apply
      v
live app descriptor + secret authorities
      |
      | Bundle Admin / supported helpers
      v
new live app state
      |
      | kdcube config export
      v
reviewable bundles.yaml + bundles.secrets.yaml

kdcube bundle config apply --descriptors-location ... reapplies the app descriptor pair. It does not modify assembly.yaml, gateway.yaml, or platform secrets.yaml.

kdcube config export --out-dir ... is the safety valve when live Bundle Admin or app writes may be newer than the seed files. User properties and user secrets are never exported into deployment descriptors.

Apply and export boundaries for application configuration Seed app descriptors apply into deployment app authorities. Deployment properties and reconstructable deployment secrets can export for review. User properties, user secrets, and request context stop at a non-export boundary. CONFIGURATION TRANSFER BOARD Deployment descriptors move. User state and request context do not. SEED DESCRIPTOR SET bundles.yaml bundles.secrets.yaml LIVE DEPLOYMENT App property authority plus Redis runtime cache App secret authority configured provider REVIEWABLE EXPORT bundles.yaml reconstructable secrets config apply config export OPERATIONAL STATE User properties User secrets bundle_call_context Durable user scope and request scope remain outside deployment descriptor export. BOUNDARY Never exported into deployment descriptors Export is a deployment-state review path, not a backup of every user or request record.
Engine Room transfer board with three lanes. Seed descriptors can be applied to deployment app authorities; live deployment app properties and reconstructable deployment app secrets can be exported for review; user properties, user secrets, and request context end at a red boundary marked "never exported to deployment descriptors."

Raw descriptor helpers such as get_plain("b:...") are for intentional descriptor inspection. They do not return the code-default merge and should not replace self.bundle_prop(...) in normal app behavior.

10One configuration, several consumers

An effective property can be data and also require application to a live runtime object.

Model-role configuration is a clear example:

code default role_models
  -> deployment override in bundles.yaml
  -> effective app properties
  -> platform applies model-router state
  -> optional bundle_call_context overlay for this invocation

The platform applies reserved model and embedding paths during property refresh. App-specific runtime objects use the async on_apply_props(props) hook. That hook runs after platform-owned application and can ask the base to rebuild model services when necessary.

The same principle applies to other long-lived objects. Reading a new endpoint property does not mutate a client already constructed with the old endpoint. The app owns a concise reconciliation step in on_props_changed or on_apply_props.

Configuration therefore has two stages:

  1. resolve one effective value set from the correct authorities;
  2. deliberately apply relevant values to runtime objects.

11Configuration review

For every app setting, answer:

  1. Is it platform, app deployment, or app user scope?
  2. Is it secret?
  3. What is the durable authority?
  4. Does app code read the effective value or intentionally inspect raw descriptor state?
  5. Does the value need to cross only this invocation through bundle_call_context?
  6. Which component must be reconstructed when the value changes?
  7. Is the value exportable, and to which descriptor?
  8. Is app code using the supported SDK helper, with async helpers for I/O, rather than environment, global state, provider internals, or blocking I/O?

When those decisions are explicit, Bundle Admin, CLI, app code, background jobs, tools, and UI all describe the same configuration system.

Read the contracts

Platform documentation:

Related public articles:

KDCube Engineering
18 · 07 · 2026