What it is
Two distinct endpoints answer two distinct questions a deployment orchestrator needs answered separately:GET /health asks “is this process running at all,” and GET /ready asks “is this
process actually able to serve real traffic right now.” Conflating them is a common mistake this
codebase deliberately avoids.
Why it was built
A process that is up but backed by dead storage is not actually ready to serve traffic, and a PaaS orchestrator that only checks liveness has no way to route around it./ready exists
specifically to catch that case and let the orchestrator act on it.
How it works
GET /health (packages/api/src/routes/health.ts) is deliberately trivial: it responds
{"status": "UP"} unconditionally, with no external check of any kind. This is a pure liveness
probe, if the process can answer HTTP at all, it answers this.
GET /ready (packages/api/src/routes/ready.ts) is genuinely different: when storage is not
Supabase-backed (NODE_ENV=test, or PARMANA_STORAGE set to anything other than "supabase" or "postgres"),
there is no external dependency to probe, so it reports READY unconditionally, the same
distinction assertStorageConfigured.ts already draws at boot. Otherwise, it actually queries
Postgres with SELECT 1 (cheap, transfers no table rows, just confirms the connection and
credentials work) via PostgresPoolFactory, not supabase-js/PostgREST, the route’s own
comment notes this probe previously depended on PostgREST for a consumed_nonces HEAD request,
meaning a PostgREST-layer outage could make the readiness probe itself unreliable; querying
directly removes that failure mode. A failed query returns 503 with status: "NOT_READY" and
reason: "storage unavailable". The error detail goes to the server log only.
Both responses also carry an authDisabled field, and a warning string when true. This exists
because PARMANA_AUTH_DISABLED=true is exactly the kind of misconfiguration (“copied
.env.example without reading every line”) that is easy to miss as a one-time console.warn at
boot but easy for an operator’s own monitoring or synthetic checks to catch and alert on
directly when it’s a field on an endpoint already polled every 30 seconds.
How it enables things, with a concrete example
examples/tutorials/89-readiness-probe/run.ts is the concrete example, read it directly, it is
short and the three scenarios described above are exactly what it runs, printing the real status
code and body for each.
How to validate this yourself
packages/api/src/routes/health.ts,packages/api/src/routes/ready.tspackages/storage/src/postgres/PostgresPoolFactory.ts(note theconnectionTimeoutMillispackages/api/src/application.ts(the lazygetPolicyRepository()/Proxyconstruction, and its own doc comment explaining why an earlier, eager version was wrong)packages/api/src/repositories.ts(lazyRepository(), the pattern the fix now mirrors)examples/tutorials/89-readiness-probe/run.tspackages/storage/tests/unit/postgres-pool-factory.test.ts(asserts the exactPoolconstructor arguments, including the timeout)
Integration requirements
No additional configuration beyond what storage already requires (PARMANA_STORAGE,
DATABASE_URL when Supabase-backed). A PaaS orchestrator should be configured to poll /health
for restart decisions and /ready for traffic-routing decisions, treating them as
interchangeable defeats the reason both exist.