Skip to content

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.

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

config feature preview

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):

Start with recipes-encrypted-tenant-config for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: none
  • Activation: always on (not toggleable)