Recipe: Managed config
What this shows: how one declarative config surface provisions everything a key needs — storage, masking, cascade, and a settings-hub entry — across two backings and two scopes, without a hand-written screen, nav, or env-wiring map.
Two keys, deliberately different:
| Key | Scope | Backing | Story |
|---|---|---|---|
payment-api-key | system | secrets | Platform-owned secret (one Stripe key bills all tenants). Stored envelope-encrypted in the secrets store, masked in every query, revealed only for the owning feature’s ctx.config read. |
smtp-host | tenant | config | Platform default that a tenant admin overrides. Cascade resolves tenant-row → system-row → default; one tenant’s override never leaks to another. |
Pattern
Section titled “Pattern”import { access, createSystemConfig, createTenantConfig } from "@cosmicdrift/kumiko-framework/engine";
r.config({ keys: { // System-only secret: storage routes to the secrets envelope (KEK rotation // + audit-on-read), never config_values. backing:"secrets" is system-only — // the boot-guard rejects it on tenant/user scope (secrets has no cascade). "payment-api-key": createSystemConfig("text", { backing: "secrets", write: access.systemAdmin, read: access.admin, mask: { title: "integrations.payment-api-key", icon: "credit-card", order: 1 }, }), // Tenant override with a platform default sourced from an env var at boot. "smtp-host": createTenantConfig("text", { env: "SMTP_HOST", default: "smtp.platform.example", write: access.roles("SystemAdmin", "Admin"), read: access.admin, mask: { title: "integrations.smtp-host", icon: "mail", order: 2 }, }), },});What each option provisions
Section titled “What each option provisions”backing: "secrets"— the value lives in the secrets store (envelope encryption, KEK rotation, audit-on-read) instead ofconfig_values. Set/read/ reset dispatch throughctx.secrets; the query handlers mask the value while the owning feature still reads the revealed plaintext viactx.config. Only valid forscope: "system"— secrets are flat per(tenant, key)with no cascade, so a tenant override would be incoherent and is rejected at boot.env: "SMTP_HOST"— at boot (runProdApp) the env var seeds the key’s default as an app-override, below anysystem-row. The env var is the default; no manualAppConfigOverridesmap, no re-typing in the admin UI.mask— the key surfaces automatically in the self-populating settings hub:buildConfigFeatureSchemaderives aconfigEditscreen + a child nav under the audience hub (Platform / Organisation / Personal) from the key’s scope and type. Nor.screen, nor.nav.mask.titleis the i18n label key.
Boot wiring
Section titled “Boot wiring”import { createConfigAccessorFactory, createConfigResolver,} from "@cosmicdrift/kumiko-bundled-features/config";import { createSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
const masterKeyProvider = createEnvMasterKeyProvider({ env: process.env });const resolver = createConfigResolver();
await runProdApp({ extraContext: ({ db, registry }) => ({ configResolver: resolver, _configAccessorFactory: createConfigAccessorFactory(registry, resolver), // Required whenever a key declares backing:"secrets" — without it the set // and read paths fail loud, never silently miss. secrets: createSecretsContext({ db, masterKeyProvider }), }), // ...});Register the secrets feature too so its tenant_secrets table is migrated;
the config backing only needs the ctx.secrets context shown above.
Vs. the neighbouring recipes
Section titled “Vs. the neighbouring recipes”| Pattern | Storage | Cascade | Use case |
|---|---|---|---|
encrypted: true config key (encrypted-tenant-config) | config_values, AES ciphertext | system → tenant | Per-tenant customer secret, no KEK rotation/audit. |
backing: "secrets" config key (this recipe) | secrets envelope, system tenant | none (system-only) | Platform-owned secret needing rotation + audit-on-read, declared as a config key. |
r.secret (secrets-demo) | secrets envelope | none | Same storage, but addressed directly — no config key, no settings-hub entry. |
backing: "secrets" is the bridge: you get the secrets store’s guarantees
and the config feature’s declarative surface (masking, settings hub, the same
config:write:set write path).
- SystemAdmin sets
payment-api-key(backing: "secrets") → value lands in the secrets envelope, masked in every query. - Owning feature reads plaintext via
ctx.config— audit-on-read from secrets. - SystemAdmin sets platform
smtp-hostdefault (system scope). - Tenant A overrides
smtp-host→ cascade resolves tenant-row for A only. - Tenant B still sees the system default — no cross-tenant leak.
bun test src/__tests__/feature.integration.test.tsProves:
backing: "secrets"routes to secrets store, notconfig_values- Owning feature reads revealed plaintext; cascade queries stay masked
- Tenant SMTP override wins locally and never leaks to another tenant
- Boot rejects
integrationsFeaturewithout thesecretsfeature mounted
What’s not in this recipe
Section titled “What’s not in this recipe”- Tenant-scoped secrets — forbidden by declaration.
backing: "secrets"+scope: "tenant"/"user"fails at boot; useencrypted: truefor a cascading per-tenant secret. - The env→default bridge at runtime —
env:is wired byrunProdApp, not bysetupTestStack, so the integration test sets the platform default explicitly via ascope: "system"write rather than through the env var.
Related samples
Section titled “Related samples”- encrypted-tenant-config —
per-tenant customer secrets with
encrypted: trueon config keys. - apps-cap-billing-demo — mail transport selected per tenant via config.
Source code
Section titled “Source code”The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):
// Managed config — one declaration provisions everything a config key needs.//// Two keys, two backings, two scopes — same declarative surface://// payment-api-key scope:"system" backing:"secrets" platform-owned secret// → stored envelope-encrypted in the secrets store (system tenant), masked// in every query, revealed only for the owning feature's ctx.config read.//// smtp-host scope:"tenant" (config, plain) per-tenant override// → platform default (env/system-row) that a tenant admin can override;// cascade resolves tenant-row → system-row → default.//// `mask` makes each key surface in the self-populating settings hub without a// hand-written r.screen / r.nav. `env` wires the platform default from an// environment variable at boot (runProdApp) — no manual AppConfigOverrides map.
import { access, type ConfigKeyHandle, createSystemConfig, createTenantConfig, defineFeature,} from "@cosmicdrift/kumiko-framework/engine";import { z } from "zod";
const FEATURE = "integrations";
export const paymentApiKeyHandle: ConfigKeyHandle<"text"> = { name: `${FEATURE}:config:payment-api-key`, type: "text",};
export const smtpHostHandle: ConfigKeyHandle<"text"> = { name: `${FEATURE}:config:smtp-host`, type: "text",};
export const integrationsFeature = defineFeature(FEATURE, (r) => { r.requires("config"); // backing:"secrets" stores payment-api-key in the secrets envelope, which // the secrets feature provisions — declare the dependency so registry-build // enforces it instead of leaving a consumer to discover the missing // tenant_secrets table at runtime. r.requires("secrets");
r.config({ keys: { // System-only secret: backing:"secrets" routes storage to the secrets // envelope (KEK rotation + audit-on-read), never config_values. The // boot-guard rejects backing:"secrets" on any non-system scope. "payment-api-key": createSystemConfig("text", { backing: "secrets", write: access.systemAdmin, read: access.admin, mask: { title: "integrations.payment-api-key", icon: "credit-card", order: 1 }, }), // Tenant override with a platform default. env seeds the default from // SMTP_HOST at boot; a tenant admin overrides it per tenant. "smtp-host": createTenantConfig("text", { env: "SMTP_HOST", default: "smtp.platform.example", write: access.roles("SystemAdmin", "Admin"), read: access.admin, mask: { title: "integrations.smtp-host", icon: "mail", order: 2 }, }), }, });
// Internal-read probe: the owning feature reads its own secrets-backed key // via ctx.config and receives the revealed plaintext, never the mask. r.queryHandler( "peek-payment-key", z.object({}), async (_query, ctx) => { if (!ctx.config) throw new Error("ctx.config not wired"); return { value: await ctx.config(paymentApiKeyHandle) }; }, { access: { roles: ["SystemAdmin"] } }, );
r.translations({ keys: { "integrations.settings": { de: "Integrationen", en: "Integrations" }, "integrations.payment-api-key": { de: "Zahlungs-API-Schlüssel", en: "Payment API Key" }, "integrations.smtp-host": { de: "SMTP-Server", en: "SMTP Host" }, }, });});📄 On GitHub: samples/recipes/managed-config/src/feature.ts