Heximon Logo
Recipes

Health Probes and a Nightly Job

Expose liveness and readiness over an app-owned controller with an injected HealthRegistry, and run a nightly report on a build-time-validated cron with ScheduledHandler.

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.

src/health/health.controller.ts
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.

src/health/health.module.ts
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.

terminal
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.

nightly-report.ts
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.

reports.module.ts
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, the Up/Degraded/Down status 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 + StorageHealthIndicator behind these two probes.
Copyright © 2026