Skip to content

Seed default data for new tenants

A tenant-specific seed creates the initial workspace data at tenant creation. It runs in the new tenant’s scope and can roll back with the tenant creation.

  • You’ve read Multi-tenancy.
  • The tenant bundled feature is loaded (auto-included in auth-mode).
  • The target write handlers are registered and accept the system actor. If the seed dispatches a background job, the app also mounts jobs and wires a job runner in its production topology.

Register an entity-wide postSave hook for the tenant entity. Use the new tenant ID when creating the system identity:

// In your seed feature's body:
import {
createSystemUser,
defineFeature,
HookPhases,
} from "@cosmicdrift/kumiko-framework/engine";
export const seedFeature = defineFeature("seed", (r) => {
r.requires("tenant");
r.requires("incidents");
r.hook("postSave", { allOf: "tenant" }, async (result, ctx) => {
if (!result.isNew) return;
const tenantId = String(result.data["id"]);
const systemForTenant = createSystemUser(tenantId, ["SystemAdmin"]);
await ctx.dispatch.writeAs(systemForTenant, "incidents:write:create", {
title: "Welcome, your first incident",
severity: "low",
body: "This is a sample. Edit or delete it.",
});
await ctx.dispatch.writeAs(systemForTenant, "incidents:write:create", {
title: "Sample: high-severity example",
severity: "high",
body: "Showing how a critical incident looks.",
});
}, {
phase: HookPhases.inTransaction,
escapeHatch: { reason: "seed starter incidents in the new tenant" },
});
});

The entity-wide hook receives SaveContext; result.isNew limits the work to tenant creation. inTransaction makes a hook failure roll back the tenant write. The hook’s escapeHatch is required because it switches to a system identity.

createSystemUser always includes the reserved system role. The extra SystemAdmin role is needed here because the target handler’s access rule is role-based. Passing the new tenant ID makes the event stream and database scope match the tenant being provisioned.

The example target is a free-form handler registered as r.writeHandler("create", ...), so its full QN is incidents:write:create. If the target is an entity convention handler from r.crud("incident", ...), its QN is instead incidents:write:incident:create. Check the registration rather than guessing the middle segment.

r.referenceData(...) is for global data such as country codes or currencies. It runs at boot. A tenant seed runs when a particular tenant is created and therefore receives that tenant’s ID.

  • Use the full target QN. ctx.dispatch.writeAs does not add the target feature prefix.
  • Declare the hook escape hatch. The hook’s own identity switch is checked independently of the handler that triggered it.
  • Keep the seed idempotent. The tenant create path may be retried. Use deterministic IDs in create payloads where the target handler supports them, or check existing state before creating rows.
  • Heavy seeds belong in an event-triggered job, not a hook. If you need to populate hundreds of rows, declare a r.job with a trigger on the qualified tenant-create handler and runIn: "worker". It runs after commit, so the job must be retry-safe. Job contexts expose ctx.dispatch.write (as the job’s system user), writeAs, queryAs and queryAsMember, but no plain query.