Skip to content

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.

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

tier-engine feature preview

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

Start with recipes-tier-admin for a step-by-step walkthrough with runnable code and integration tests.

  • Requires: config, tenant
  • Activation: always on (not toggleable)