Saltar al contenido principal

Health Module

Overview​

The health module exposes lightweight diagnostic endpoints used by:

  • Cloud Run liveness/readiness probes — GET /health and GET /api/v1/pricing/health/live
  • Operations & on-call — GET /health/database for detailed PostgreSQL diagnostics
  • Pricing subsystem probes — GET /api/v1/pricing/health/*

All health endpoints are public (@IsPublic()) and require no authentication.


Architecture​

The module follows the standard hexagonal (ports & adapters) layout:

health/
├── health.module.ts # Module definition
├── domain/
│ └── database-health.domain.ts # Port: IDatabaseHealthChecker + DatabaseHealthResult
├── application/
│ └── health.service.ts # Use case: orchestrates health responses
├── infrastructure/
│ ├── database-health.repository.ts # Adapter: Kysely-based PostgreSQL health checks
│ └── __tests__/
│ └── database-health.repository.spec.ts # Unit tests for the DB adapter
└── interfaces/
├── health.controller.ts # GET /health, GET /health/database
├── pricing-health.controller.ts # GET /api/v1/pricing/health/*
└── dtos/
└── health.dto.ts # Swagger response schemas

Layer responsibilities:

LayerResponsibility
domain/IDatabaseHealthChecker interface (port) and DatabaseHealthResult value object — no framework dependencies
application/HealthService orchestrates health use cases, depends only on the domain port
infrastructure/DatabaseHealthRepository implements IDatabaseHealthChecker using Kysely sql queries
interfaces/Thin controllers that map HTTP requests → application service → response DTOs

Dependency injection:

The DATABASE_HEALTH_CHECKER symbol token binds DatabaseHealthRepository to IDatabaseHealthChecker in the module providers. Controllers and the application service depend on the interface, never on the concrete implementation.


Endpoints​

GET /health​

Liveness probe for Cloud Run.

Always returns HTTP 200 and status: "ok" so the container is never restarted due to a database blip. Runs a timed SELECT 1 ping (2s cap). Inspect database.isConnected for actual DB health. Full Cloud SQL diagnostics (databaseName, serverVersion, userTableExists, …) are on GET /health/database.

{
"status": "ok",
"version": "0.0.145",
"firebaseProjectId": "flowpos-prod",
"database": {
"isConnected": true,
"responseTime": 4,
"userTableExists": false,
"databaseName": null,
"serverAddress": null,
"serverPort": null,
"currentUser": null,
"serverVersion": null,
"connectionMethod": null
},
"timestamp": "2026-03-12T00:00:00.000Z"
}

GET /health/database​

Detailed database diagnostics.

Returns status: "error" when the database is unreachable. Useful for debugging Cloud SQL VPC connector issues in staging/production.

{
"status": "ok",
"database": { ... },
"timestamp": "2026-03-12T00:00:00.000Z"
}

GET /api/v1/pricing/health​

Basic liveness for the pricing subsystem. Returns status: "healthy" as long as the process is running.

GET /api/v1/pricing/health/detailed​

Reports three component statuses:

ComponentHealthy when
databaseIDatabaseHealthChecker.ping() returns isConnected: true
cachePrice-cache hit rate ≥ 50 % after 100+ calls
featureFlagAlways healthy — reports ENABLE_PRICE_LISTS value

Overall status is the worst of all components (healthy → degraded → unhealthy).

GET /api/v1/pricing/health/ready​

Readiness probe. Returns ready: true only when the database is reachable. Cloud Run will stop routing traffic if this returns ready: false.

GET /api/v1/pricing/health/live​

Liveness probe. Always returns alive: true. If this stops responding, Cloud Run restarts the container.


Cloud Run probe configuration​

Both controllers follow the Cloud Run probe contract:

  • Liveness (GET /health, GET /api/v1/pricing/health/live) — always 200; never kill the container due to DB downtime
  • Readiness (GET /api/v1/pricing/health/ready) — 200 only when DB is healthy

Recommended Cloud Run settings:

livenessProbe:
httpGet:
path: /health
initialDelaySeconds: 10
periodSeconds: 30

readinessProbe:
httpGet:
path: /api/v1/pricing/health/ready
initialDelaySeconds: 5
periodSeconds: 10

Design decisions​

Why does GET /health always return 200? Cloud Run will restart the container if the liveness probe returns a non-2xx. A database blip should not cause a container restart — the app itself is healthy; only the DB connection is degraded. Operators should monitor database.isConnected via alerting rather than relying on container restarts.

Why follow hexagonal architecture for a simple health module? Consistency with all other backend modules. The domain port (IDatabaseHealthChecker) makes the health service testable without Kysely and allows swapping the DB check implementation without touching controllers or application logic.

Why does PricingHealthController live in HealthModule and not PricingModule? The readiness and liveness probes must always be available even if PricingModule fails to bootstrap. Hosting them in the always-loaded HealthModule guarantees the probes are reachable during partial startup failures.


Bruno API collection​

All six endpoints are covered by Bruno requests at:

api-client/flowpos/collections/health/

Set BASE_URL to http://localhost:4000 in your Bruno environment. No auth headers are required.