Health Probes and a Nightly Job
Two small operational pieces most services need on day one: a /health + /ready pair an orchestrator can
poll, and a job that runs on a wall-clock schedule instead of waiting for a request. Neither needs a compiler
plugin beyond the ones you already register for HTTP — @heximon/health never mounts routes itself, and
@heximon/schedule validates your cron expression at build time so a typo fails the compile, not a 3am page.
1. Expose the probes over your own controller
@heximon/health discovers your HealthIndicator classes and aggregates them into an injectable
HealthRegistry — nothing more. You own the HTTP surface: paths, status codes, response shape.
import { type AggregatedHealthResult, HealthRegistry } from "@heximon/health";
import type { Controller, Get } from "@heximon/http";
export class HealthController implements Controller {
public constructor(private readonly health: HealthRegistry) {}
public live(_action: Get<"/health">): { status: string } {
return { status: "up" };
}
public async ready(action: Get<"/ready">): Promise<AggregatedHealthResult> {
const result = await this.health.checkAll();
if (result.status === "down") {
action.response.status = 503;
}
return result;
}
}
(Source: examples/flagship/packages/app/src/health/health.controller.ts)
/health returns immediately with no indicator run and no boot promise awaited — safe for a liveness probe
that fires while the app is still starting. /ready runs every registered indicator and maps down to 503
so an orchestrator stops routing traffic there.
2. Register the shipped indicators
List indicator classes under health: { indicators } — never in providers, or the compiler treats them as
plain injectables instead of routing them into the registry. The two shipped indicators are bare providers:
their constructors are plain DI tokens, so the compiler recovers them straight from the package types.
import { Module } from "@heximon/runtime";
import { DatabaseHealthIndicator } from "@heximon/health/database";
import { StorageHealthIndicator } from "@heximon/health/storage";
import { Storage } from "@heximon/kv";
import { Database } from "@heximon/persistence";
import { HealthController } from "./health.controller";
export class HealthModule extends Module({
requires: [Database, Storage],
http: { controllers: [HealthController] },
health: { indicators: [DatabaseHealthIndicator, StorageHealthIndicator] },
}) {}
(Source: examples/flagship/packages/app/src/health/health.module.ts)
DatabaseHealthIndicator pings the pool via an empty transaction; StorageHealthIndicator round-trips a
sentinel key. Both inject an abstract port (Database, Storage) rather than a concrete class, which is why
this module lists them under requires — the app composing HealthModule must supply a satisfier for each.
curl http://localhost:3000/health
# { "status": "up" }
curl http://localhost:3000/ready
# { "status": "up", "checks": { "database": { "status": "up" }, "storage": { "status": "up" } } }
3. Declare the nightly job
A scheduled job is a class whose cron expression is a string-literal type argument — the compiler reads it as the dispatch key at build time, so a malformed cron is a build error, not a job that silently never fires.
import type { ScheduledHandler } from "@heximon/schedule";
import { ReportsRepository } from "./reports.repository";
export class NightlyReport implements ScheduledHandler<"0 3 * * *", { timezone: "UTC"; name: "nightly-report" }> {
public constructor(private readonly reports: ReportsRepository) {}
public handle(scheduledAt: Date): void {
this.reports.record(scheduledAt);
}
}
(Source: docs/content/docs/03.capabilities/04.scheduled-jobs.md, already-verified fence)
handle always receives the platform's scheduled time, never Date.now() — a late or replayed fire still
computes against the intended wall-clock instant. { timezone, name } ride the second type argument;
timezone defaults to UTC.
4. List it under the schedule namespace
Same rule as indicators: the handler goes under schedule: { handlers }, never providers — that's what
routes it to its cron instead of constructing it as a plain injectable.
import { Module } from "@heximon/runtime";
import { NightlyReport } from "./nightly-report";
import { ReportsController } from "./reports.controller";
import { ReportsRepository } from "./reports.repository";
export class ReportsModule extends Module({
providers: [ReportsRepository],
http: {
controllers: [ReportsController],
},
schedule: {
handlers: [NightlyReport],
},
}) {}
(Source: docs/content/docs/03.capabilities/04.scheduled-jobs.md, already-verified fence)
On Node, NodeScheduleDriver self-starts one process-local timer per schedule at boot — no host wiring, and
it's the tier pnpm dev runs under. Overlap is skip-if-running, so a slow nightly job never stacks two runs.
On a Cloudflare deploy, the same cron list is emitted into wrangler.json's triggers.crons instead, and the
platform's own clock fires your handler.
Pitfalls
Registering a handler under providers instead of schedule: { handlers }. It still constructs — DI
doesn't complain — but nothing ever calls it. The compiler only routes classes to their cron when they're
listed under the schedule namespace.
Expecting exactly-once delivery. Scheduled dispatch is at-least-once on Cloudflare and best-effort on Node
(a process down across the tick misses that run). Key handle off scheduledAt — stable across replays of the
same tick — if the job needs to be idempotent.
See also
- Health & Readiness — writing your own
HealthIndicator, theUp/Degraded/Downstatus table, and customizing a shipped indicator with a delegating wrapper. - Scheduled Work — per-host firing semantics, the Cloudflare cron-trigger path, and multi-replica considerations.
- Flagship Health / Readiness — the
runnable
DatabaseHealthIndicator+StorageHealthIndicatorbehind these two probes.
Protect Admin Routes
Gate an admin-only surface with JWTAuthMiddleware for authentication and AuthContext.isAllowed for a scope-guarded 401/403/200 ladder on inline routes.
Build a CRUD API
A typed CRUD feature end-to-end — a zod-validated body, an inline-route Controller, a plain repository over Drizzle, boot-time table creation, and an in-process test.