Skip to main content

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.