config
Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row → tenant-row → system-row → app-override (deploy-time AppConfigOverrides) → computed → feature default. Access a value in handlers via ctx.config(handle), declare keys with r.config({ keys: { ... } }) inside a feature’s registry callback, and optionally mark them encrypted: true to route storage through the envelope cipher (versioned master key). Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.
Quick example
Section titled “Quick example”From recipes-encrypted-tenant-config — the smallest working mount:
// Encrypted per-tenant config — Stripe-API-key pattern.//// Minimal-Showcase: ein dummy "billing"-feature mit einem per-tenant// Stripe-API-Key. Der `mask`-Eintrag lässt den configEdit-Screen +// Settings-Hub-Nav automatisch entstehen (kein handgeschriebenes// r.screen/r.nav mehr). charge-handler liest den Key über ctx.config// (entschlüsselt automatisch).//// Production: nicht aufrufen — der echte Stripe-Call ist hier ein Mock.// Pattern in produktiven Apps: Strip-API-Key in tenantBillingConfig.key,// charge-handler lädt key + ruft tatsächlich Stripe.
import { access, type ConfigKeyHandle, createTenantConfig, defineFeature, defineWriteHandler,} from "@cosmicdrift/kumiko-framework/engine";import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";import { z } from "zod";
const FEATURE = "billing";
// Config-Key-Definition: encrypted=true → ciphertext in der DB. write/// read: access.admin damit nur Tenant-Admin den Key setzt + reads sind// nur backend-side via ctx.config (frontend sieht "••••••"). `mask` →// buildConfigFeatureSchema derivt Screen + Settings-Hub-Nav.const stripeApiKeyDef = createTenantConfig("text", { encrypted: true, write: access.admin, read: access.admin, // mask-derived screens carry no field constraints (maxLength, pattern) — // set those separately in a configEdit schema if you need them enforced. mask: { title: "billing.stripe-api-key", order: 1 },});
const stripeApiKeyHandle: ConfigKeyHandle<"text"> = { name: `${FEATURE}:config:stripe-api-key`, type: "text",};
// Charge-Handler — nutzt den entschlüsselten API-Key. Caller sieht NUR// die charge-id zurück, der Key bleibt server-side.const chargeHandler = defineWriteHandler({ name: "charge", schema: z.object({ amount: z.number().positive(), customerRef: z.string().min(1), }), access: { roles: ["Admin"] }, async handler(event, ctx) { if (!ctx.config) { return writeFailure( new UnprocessableError("config_unavailable", { i18nKey: "billing.errors.configUnavailable", }), ); } const apiKey = await ctx.config(stripeApiKeyHandle); if (!apiKey || apiKey.length === 0) { return writeFailure( new UnprocessableError("stripe_key_missing", { i18nKey: "billing.errors.stripeKeyMissing", }), ); }
// Mock: real impl würde fetch("https://api.stripe.com/v1/charges", // { headers: { Authorization: `Bearer ${apiKey}` }, ... }) ausführen. // Wichtig: apiKey verlässt den server NICHT — kein log, kein // response-field, kein error-detail. const chargeId = `ch_${Date.now()}_${event.payload.customerRef}`;
return { isSuccess: true as const, data: { chargeId }, }; },});
export const billingFeature = defineFeature(FEATURE, (r) => { r.requires("config"); r.config("stripe-api-key", stripeApiKeyDef); r.writeHandler(chargeHandler); // Kein r.screen/r.nav: der `mask`-Eintrag auf dem Key lässt // buildConfigFeatureSchema den configEdit-Screen (••••••-maskiert, // config:write:set verschlüsselt vor dem write) + den Settings-Hub-Nav // automatisch ableiten.
r.translations({ keys: { "billing.settings": { de: "Abrechnung", en: "Billing" }, "billing.stripe-api-key": { de: "Stripe-API-Schlüssel", en: "Stripe API Key" }, }, });});
// Re-exports damit tests die handles ohne re-typing nutzen können.export { stripeApiKeyHandle };📄 On GitHub: samples/recipes/encrypted-tenant-config/src/feature.ts
Live preview
Section titled “Live preview”
How it fits
Section titled “How it fits”The write commands this feature provides (Provides).
flowchart TB
n_config["config"]
subgraph how_provides["Provides"]
n_cmd_config_write_reset(["reset"])
n_cmd_config_write_set(["set"])
end
n_config --> n_cmd_config_write_reset
n_config --> n_cmd_config_write_set
Provides — write commands this feature registers (dispatch them through the command bus):
Getting started
Section titled “Getting started”Start with recipes-encrypted-tenant-config for a step-by-step walkthrough with runnable code and integration tests.
Dependencies
Section titled “Dependencies”- Requires: none
- Activation: always on (not toggleable)