Hooks
Every service receives lifecycle on TServiceParams. It exposes seven registration methodsโone per stage. All accept an optional priority number.
type TLifeCycleRegister = (callback: () => void | Promise<void>, priority?: number) => void;
onPreInitโ
Runs at PreInit, after all services are wired and before configuration initialization. External env/argv/file values and custom loader results have not settled yet.
Legacy defaults are available. An explicit TypeBox root default may be available during construction, and raw bootstrap values can be visible, but schema previews are not guaranteed processed output. Use this stage for early setup and custom loader registration:
export function RemoteConfigService({ internal, lifecycle }: TServiceParams) {
lifecycle.onPreInit(() => {
internal.boilerplate.configuration.registerLoader(
async () => ({ my_app: { PORT: "8080" } }),
"remote",
);
});
}
onPostConfigโ
Runs after every enabled loader has settled, native schemas have processed their winning inputs, and one complete configuration snapshot has been published. This is the first stage where final config.* values are guaranteed:
export function ApiService({ config, lifecycle, logger }: TServiceParams) {
lifecycle.onPostConfig(() => {
logger.info({ port: config.my_app.PORT }, "config loaded");
});
}
A legacy required: true failure throws REQUIRED_CONFIGURATION_MISSING before this stage. A schema failure throws a CONFIG_SCHEMA_* error before this stage. Nested TypeBox defaults/effects and Zod or opaque Standard Schema defaults are also finalized by this point.
onBootstrapโ
Runs at Bootstrap, after PostConfig. It is safe to open connections, load data, and start background work. It supports async callbacks.
lifecycle.onBootstrap(async () => {
await db.connect();
await cache.warmUp();
});
onReadyโ
Runs at Ready, after all Bootstrap callbacks. It is safe to serve traffic, start cron jobs, and emit an application-ready notification:
export function ServerService({ config, lifecycle, logger }: TServiceParams) {
lifecycle.onReady(() => {
server.listen(config.my_app.PORT);
logger.info({ port: config.my_app.PORT }, "server listening");
});
}
The scheduler activates at Ready; jobs registered earlier begin scheduling from this stage onward.
onPreShutdownโ
Runs at PreShutdown when teardown starts. Stop accepting new work:
lifecycle.onPreShutdown(() => {
server.close();
queue.pause();
});
onShutdownStartโ
Runs at ShutdownStart. Flush and close resources; async callbacks are supported:
lifecycle.onShutdownStart(async () => {
await db.end();
await queue.drain();
await cache.flush();
});
onShutdownCompleteโ
Runs at ShutdownComplete for final best-effort cleanup. Errors here are logged without changing the shutdown sequence:
lifecycle.onShutdownComplete(() => {
logger.info("shutdown complete");
});
Priorityโ
All hook methods accept an optional second argument. See Execution Order for priority semantics.
lifecycle.onBootstrap(connectDatabase, 100);
lifecycle.onBootstrap(optionalMetrics, -10);
Late registrationโ
If a startup stage has already completed, a newly registered callback fires immediately. If a shutdown stage has already completed, a newly registered callback is silently dropped.
TParentLifecycle.child()โ
Core exports the extended TParentLifecycle interface, which declares child(): TLifecycleBase. When an integration supplies this extended interface, its child lifecycle exposes the same hook registration methods:
import type { TParentLifecycle } from "@digital-alchemy/core";
function registerScopedHooks(parent: TParentLifecycle) {
const childLifecycle = parent.child();
childLifecycle.onBootstrap(() => { /* scoped setup */ });
childLifecycle.onShutdownStart(() => { /* scoped teardown */ });
}
The ordinary lifecycle injected through TServiceParams is TLifecycleBase; it does not expose child(). The provider of a TParentLifecycle is responsible for creating and driving its child scopes.
For the full configuration preview and finalization model, see Accessing Config and Schema-backed Configuration.