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.});
// 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”What this feature needs to run (Requires, top) and the write commands it provides (Provides, bottom).
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)