Lifecycle and hooks
In plain words
Section titled “In plain words”A Kumiko process moves through four states: starting, ready, draining, and stopped. It validates environment values and the feature graph before opening database connections in boot-check mode or serving traffic in a real boot.
During shutdown, the process becomes unready, stops background consumers, and
closes resources through registered shutdown hooks. The hooks run in reverse
registration order and share a deadline. /health tells an orchestrator that
the process is alive; /health/ready tells it whether traffic should be sent to
that process.
starting -> ready -> draining -> stoppedWhy it exists
Section titled “Why it exists”Every API, worker, and all-in-one entry point uses the same four-state lifecycle. Features declare work through the registrar instead of inventing a separate process scheduler. Operators get predictable boot failures and a readiness signal that changes during drain.
This page covers process lifecycle and boot validation. The transaction phases
of preSave, postSave, and delete hooks are covered in
Events and projections.
What runs at boot
Section titled “What runs at boot”The production entry point performs these groups in order:
- Validate the composed environment schema. A render-only mode can print the required values and exit.
- Resolve Key Manager ciphertext slots and load required environment values.
- Compose features, validate the feature graph, and build the registry.
- In boot-check mode, report success and exit without opening database or Redis connections.
- Check the subject-key provider, then connect to PostgreSQL and Redis.
- Verify that checked-in schema migrations are applied and expected tables exist.
- Build the dispatcher, auth routes, jobs, event consumers, file providers, and other runtime services.
- Apply idempotent boot seeds and pending seed migrations.
- Start event consumers and job runners, build the HTTP handler, then mark the lifecycle ready.
The diagram groups some implementation details more finely, but the ordering
rule is the same: validation precedes traffic, and schema drift stops boot.
Production does not apply schema migrations automatically. The deployment runs
kumiko schema apply; runProdApp verifies the result.
Boot validation
Section titled “Boot validation”Before a real boot opens PostgreSQL or Redis, validateBoot checks the complete
feature graph. Examples include:
| Check | Failure caught before traffic |
|---|---|
Every r.requires("x") resolves | Missing hard dependency |
| Handler and screen references resolve | Typo in a qualified name |
| Dependencies contain no cycles | A requires B, which requires A |
| Entity and screen names do not collide | Duplicate registration |
| Sort and filter fields support the requested operation | Invalid list configuration |
| Ownership rules name existing roles, claims, and columns | Silent access mismatch |
| Registrar extension use declares its provider dependency | Extension loaded without its owner |
| Personal-data annotations and KMS wiring agree | Plaintext or unshreddable data path |
Validation reports a collection of findings where possible. A failed check keeps
the lifecycle in starting and the process exits non-zero.
Run the same graph check without database connections in CI:
bun run bootWhere feature code attaches
Section titled “Where feature code attaches”Feature code uses registrar declarations:
r.hook(...)attaches validation and entity lifecycle behavior.r.job(...)declares event-driven, cron, boot, or manual queue work.r.projection(...)declares an in-transaction read model.r.multiStreamProjection(...)declares an asynchronous event consumer.r.requires(...)and extensions describe boot-time relationships.
A feature does not register a new process startup phase. Runtime entry points start and stop the declared jobs and event consumers.
Write lifecycle hooks
Section titled “Write lifecycle hooks”A postSave or postDelete hook chooses a phase:
inTransactionruns before commit. Throwing rolls back the write.afterCommitruns after commit. Throwing is logged and cannot undo the write.
preSave and preDelete run inside the write transaction. Use these hooks for
entity behavior, not for process startup.
Scheduled and queued jobs
Section titled “Scheduled and queued jobs”BullMQ stores job queues and cron schedulers in Redis. A job’s runIn setting
chooses the API or worker lane. Event-triggered and manual jobs use the same
queue infrastructure. Features declare triggers and concurrency policy; they do
not elect a process leader themselves.
Graceful shutdown
Section titled “Graceful shutdown”SIGTERM and SIGINT call the entry point’s stop path. The lifecycle changes to
draining, so readiness returns 503. Registered shutdown hooks then run in
last-in, first-out order. Job runners and event consumers stop through these
hooks before the entry point closes PostgreSQL and Redis.
Lifecycle drain waits up to 40 seconds by default. It logs hook failures and continues with the remaining hooks. After the deadline it marks the process stopped even if one hook did not finish. Kubernetes needs a termination grace period long enough for that drain.
Health endpoints
Section titled “Health endpoints”/health is the liveness endpoint. It returns 200 while the process is alive,
including during startup or drain. Kubernetes uses it to decide whether the
container needs a restart.
/health/ready is the readiness endpoint when a lifecycle is wired. It returns
200 only in the ready state. During startup, drain, or a failed registered
readiness check it returns 503, and the load balancer stops sending new traffic.
Operational effect
Section titled “Operational effect”A rolling deployment can start a new pod, wait for readiness, then drain the old pod. A feature-graph or schema error prevents the new pod from becoming ready while the old replica continues serving when the deployment strategy and capacity allow it.
Boot validation cannot prove that an external provider is semantically correct or that a business probe succeeds. Deployment verification still needs health checks plus one real application read and write.
Live example
Section titled “Live example”The lifecycle-hooks recipe registers validation, save, and delete hooks:
const editorWrite = { access: { roles: ["Admin", "Editor"] } } as const;
export const articlesFeature = defineFeature("blog", (r) => { const article = r.entity({ name: "article", ...articleEntity });
const articleCreate = r.writeHandler( defineEntityCreateHandler("article", articleEntity, editorWrite), ); const articleUpdate = r.writeHandler( defineEntityUpdateHandler("article", articleEntity, editorWrite), ); r.writeHandler(defineEntityDeleteHandler("article", articleEntity, editorWrite));
// --- scheduleAfterRollback: give back a reservation made outside the transaction when it does // not commit. The callback must be idempotent: a failed COMMIT also triggers it. r.writeHandler({ name: "article:reserve-slot", schema: z.object({ slot: z.string(), reject: z.boolean() }), access: editorWrite.access, handler: async (event, ctx) => { const { slot, reject } = event.payload; reservedSlots.add(slot); ctx.lifecycle.afterRollback?.(async () => { reservedSlots.delete(slot); }); if (reject) return writeFailure(new UnprocessableError("slot_rejected")); return { isSuccess: true as const, data: {} }; }, });Full source: samples/recipes/lifecycle-hooks.
See also
Section titled “See also”- Features and composition, the graph validated at boot.
- Events and projections, transaction phases and replay.
- Deploy to K3s, readiness and rollback in production.