tier-engine
Stores a tier-assignment entity per tenant (which pricing tier is active) and, when configured with a TierMap, registers itself as the tenantTierResolver extension so the dispatcher automatically gates r.toggleable() features per tenant based on their assigned tier. Call createTierEngineFeature({ defaultTier, tierMap }) to get full tier composition — including an inTransaction entity hook that atomically writes the default tier when a new tenant is created — or use createTierEngineFeature() without options for storage-only mode when you manage tier assignment yourself via composeApp. A SystemAdmin-only set-tenant-tier write plus get-tenant-tier/tier-options reads let an operator assign a tier to ANY tenant manually — without a billing purchase — stamping source: "manual" so a future Stripe→tier sync won’t overwrite the grant. Apps surface this via the tier-admin screen.
Quick example
Section titled “Quick example”From recipes-tier-admin — the smallest working mount:
// Tier Admin Sample//// Shows the SystemAdmin-only operator flow: assigning a tier to *any*// tenant without a billing purchase. The app side is intentionally tiny —// the recipe is the bundled tier-engine itself, configured with the app's// own TierMap. The integration test exercises the cross-tenant grant// (set-tenant-tier), the read-back (get-tenant-tier returning// source:"manual"), and the option-list (tier-options) end-to-end.
import { createTierEngineFeature, type TierMap,} from "@cosmicdrift/kumiko-bundled-features/tier-engine";import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
// --- App caps ---//// Each app picks its own cap dimensions. Here a tiny example: how many// notes a tenant may keep. The TierMap is generic in this cap-shape so// downstream code stays type-safe end-to-end.
export type AppCaps = { readonly maxNotes: number };
// --- The toggleable feature a paid tier unlocks ---//// A `r.toggleable()` feature shows up in a tenant's effective-features set// exactly when its tier lists it. "pro" lists it below; "free" does not.// This is what makes the cache-sync invariant observable: granting "pro"// must light this feature up in the resolver the same request, not after a// refresh, replay, or restart.
export const NOTES_EXPORT_FEATURE = "notes-export";
export const notesExportFeature = defineFeature(NOTES_EXPORT_FEATURE, (r) => { r.toggleable({ default: false });});
// --- Tier map ---//// "free" + "pro" — the operator picks one of these names when granting// a tier manually. "pro" unlocks the notes-export toggleable feature; the// integration test grants "pro" and then reaches that feature in the same// request, proving the cache-sync invariant end-to-end.
export const appTierMap: TierMap<AppCaps> = { free: { features: [], caps: { maxNotes: 5 } }, pro: { features: [NOTES_EXPORT_FEATURE], caps: { maxNotes: 100 } },};
// --- Configured tier-engine ---//// `defaultTier: "free"` means every new tenant starts on free via the// `inTransaction` entity hook the tier-engine registers — no app code// needed. `tierMap` makes `tier-options` return ["free", "pro"] so the// tier-admin screen can populate its picker without hard-coding.
export const tierEngineForApp = createTierEngineFeature<AppCaps>({ defaultTier: "free", tierMap: appTierMap,});📄 On GitHub: samples/recipes/tier-admin/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_tier_engine["tier-engine"]
subgraph how_reqs["Requires"]
n_config["config"]
n_tenant["tenant"]
end
subgraph how_provides["Provides"]
n_cmd_tier_engine_write_set_tenant_tier(["set-tenant-tier"])
n_cmd_tier_engine_write_tier_assignment_create(["create"])
n_cmd_tier_engine_write_tier_assignment_update(["update"])
end
n_config --> n_tier_engine
n_tenant --> n_tier_engine
n_tier_engine --> n_cmd_tier_engine_write_set_tenant_tier
n_tier_engine --> n_cmd_tier_engine_write_tier_assignment_create
n_tier_engine --> n_cmd_tier_engine_write_tier_assignment_update
Provides — write commands this feature registers (dispatch them through the command bus):
tier-engine:write:set-tenant-tiertier-engine:write:tier-assignment:createtier-engine:write:tier-assignment:update
Getting started
Section titled “Getting started”Start with recipes-tier-admin for a step-by-step walkthrough with runnable code and integration tests.