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.
Prerequisites
Section titled “Prerequisites”- You’ve read Multi-tenancy.
- The
tenantbundled feature is loaded (auto-included inauth-mode). - The target write handlers are registered and accept the system actor. If the
seed dispatches a background job, the app also mounts
jobsand wires a job runner in its production topology.
The code
Section titled “The code”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.
Why not seed at boot
Section titled “Why not seed at boot”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.
Common gotchas
Section titled “Common gotchas”- Use the full target QN.
ctx.dispatch.writeAsdoes 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.jobwith a trigger on the qualified tenant-create handler andrunIn: "worker". It runs after commit, so the job must be retry-safe. Job contexts exposectx.dispatch.write(as the job’s system user),writeAs,queryAsandqueryAsMember, but no plainquery.
See also
Section titled “See also”- Multi-tenancy, the tenant entity and the system identity.
- Lifecycle and hooks,
inTransactionvs.afterCommitsemantics.