Health Module
Overview
The health module exposes lightweight diagnostic endpoints used by:
- Cloud Run liveness/readiness probes —
GET /healthandGET /api/v1/pricing/health/live - Operations & on-call —
GET /health/databasefor 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:
| Layer | Responsibility |
|---|---|
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. Inspect database.isConnected for actual DB health.
{
"status": "ok",
"version": "0.0.145",
"firebaseProjectId": "flowpos-prod",
"database": {
"isConnected": true,
"responseTime": 12,
"userTableExists": true,
"databaseName": "flowpos_production",
"serverAddress": null,
"serverPort": null,
"currentUser": "flowpos_production",
"serverVersion": "PostgreSQL 17.0 ...",
"connectionMethod": "Unix Socket"
},
"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:
| Component | Healthy when |
|---|---|
database | IDatabaseHealthChecker.testConnection() returns isConnected: true |
cache | Price-cache hit rate ≥ 50 % after 100+ calls |
featureFlag | Always 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.