# Add Health Checks to a Phoenix App

Yes. An agent adds liveness and readiness endpoints to your Phoenix app, checks the database behind the readiness one, writes ExUnit tests, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-health-checks/phoenix/), in a Project that has your repository.

## What the agent does

The agent starts with the router, because in a Phoenix app with a catch-all live route the place a health route is declared decides whether it works. In an app like `fly-apps/live_beats`, `lib/live_beats_web/router.ex` defines a `:browser` pipeline and an `:api` pipeline that accepts JSON but is used by no scope, and its main scope declares `live "/:profile_username", ProfileLive, :show` inside an `:authenticated` live session. A `GET /health` added after that scope would match the profile route and redirect to sign-in. `mix.exs` pins Phoenix `~> 1.7.1`, `ecto_sql ~> 3.11` and `postgrex`, and `mix.lock` resolves Phoenix to `1.7.12` and Plug to `1.16.0`. `lib/live_beats_web/endpoint.ex` already runs `Plug.RequestId` and `Plug.Telemetry`.

It then reads what the deployment checks today and what the app depends on. `fly.toml` has a `[[services.tcp_checks]]` block on internal port 4000 every fifteen seconds with a twenty-second grace period, so the platform only knows the port is open, not that the app can reach Postgres. `config/runtime.exs` requires `DATABASE_URL` in production and sets the pool to `POOL_SIZE` or five, and `lib/live_beats/application.ex` starts `LiveBeats.Repo`, `Phoenix.PubSub`, `Presence`, `Finch`, a `FLAME.Pool` and the endpoint under one supervisor. `lib/live_beats/repo.ex` is a plain Postgres repo, so a readiness check is a `Repo.query("SELECT 1")` with a short timeout, nothing more.

From that it writes `lib/live_beats_web/controllers/health_controller.ex` with two actions. `live` answers `200` with `{"status": "ok"}` and touches nothing, so it tells an orchestrator only that the BEAM is serving requests. `ready` runs the `SELECT 1` through the repo with a timeout well under the check interval and answers `200` with `{"status": "ok", "checks": {"database": "ok"}}` or `503` with the same shape and `"error"` when the query fails or times out, catching the `DBConnection` errors rather than letting them become a 500. The router gets a `scope "/health"` that pipes through `:api` and is declared before the browser scopes so the paths never reach the profile route. `fly.toml` gains a `[[services.http_checks]]` block on `/health/ready` with the existing interval, timeout and grace period, and the pull request explains that a readiness failure then takes the instance out of routing while the database is unreachable.

Then it writes `test/live_beats_web/controllers/health_controller_test.exs` on `LiveBeatsWeb.ConnCase`, the same case `redirect_controller_test.exs` uses: one test that `GET /health/live` returns the JSON with status 200, one that `GET /health/ready` returns 200 with the database check `ok` against the sandboxed test repo, and one that makes the check fail by pointing the controller's query function, which it reads from application config, at one that raises, and asserts the 503 body. It starts Postgres in the sandbox to match `config/test.exs`, runs `mix test`, which the alias in `mix.exs` chains behind `ecto.create` and `ecto.migrate`, runs `mix format --check-formatted`, and fixes anything until both pass.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add health check endpoints to this Phoenix app. Read mix.exs, mix.lock, lib/live_beats_web/router.ex, lib/live_beats_web/endpoint.ex, lib/live_beats/application.ex, lib/live_beats/repo.ex, config/runtime.exs, config/test.exs, fly.toml, test/support/conn_case.ex and test/live_beats_web/controllers/redirect_controller_test.exs first. Add a HealthController with a live action that returns 200 and JSON without touching any dependency, and a ready action that runs SELECT 1 through the repo with a short timeout and returns 200 with a checks map, or 503 with the same shape when the query fails or times out, catching the database errors instead of raising. Mount them under /health through the api pipeline, declared before any scope with a catch-all live route. Replace the tcp check in fly.toml with an http check on the readiness path using the existing interval, timeout and grace period. Write ExUnit tests on ConnCase for both actions, including one that forces the database check to fail. Start Postgres in the sandbox, run mix test and mix format --check-formatted, fix anything until they pass, and open a pull request that explains what each endpoint means and how the platform check was changed.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with the new controller, the `/health` scope in the router, the `fly.toml` change and the test file. The description says what each endpoint means to an orchestrator, why the scope is declared where it is, what the readiness timeout is and why it is shorter than the check interval, and that a readiness failure now removes the instance from routing where the TCP check kept it in.

The transcript on the session keeps every command with its output: the Postgres start, `mix deps.get`, the `mix test` run with the new tests, the formatter check, and the `curl` calls against a running `mix phx.server` showing the two JSON bodies and the 503 when Postgres is stopped. There is no CI workflow in this repository, so that transcript is the test record for the change.

Before merging, a reviewer checks that the readiness query timeout fits the pool settings in `config/runtime.exs`, that the platform check points at readiness and not liveness, that nothing in the readiness path needs authentication or a session, and that the two routes are reachable in a release build where `Mix.env()` guards are gone. One follow-up message to the same session adds a check for another dependency, such as the uploads mount under `BUCKET_MOUNT`, to the same `checks` map.

## Run it on every Linear issue labelled observability

A team that tracks operational work in Linear starts one run per issue from an automation with a `linear` trigger on the `observability` label. The automation adds the issue after this prompt, so the agent reads which dependencies the issue wants checked and starts in the repository the automation names.

Each run opens its own pull request from its own sandbox, and an automation run has its own `max_runs_per_hour` cap. The pull request links back to the issue, and the transcript holds the test run and the live requests for that change.

## Why run it on Rush?

The code is short; the checking is not. Proving a readiness endpoint means starting Postgres, running the app, stopping Postgres and watching the endpoint answer 503 instead of hanging or crashing the request, then running the suite with the sandbox adapter. On a laptop that competes with the database already running for development. In a Rush sandbox the agent has its own Postgres and its own port 4000, and the sandbox parks when the turn ends.

The pull request carries its own proof. The JSON from both endpoints, the 503 with the database down and the green `mix test` run are in the transcript beside the commands that produced them, so a reviewer can see the endpoint behaves under the failure it exists to report.

## Frequently asked questions

### Why two endpoints instead of one /health?

Because they answer different questions. Liveness says the VM is serving requests and should never fail because a dependency is down, or an orchestrator would restart a healthy process. Readiness says the app can do useful work right now, which for this app means reaching Postgres, and a failure there should pull the instance out of routing until the database is back.

### Why is the route declared before the browser scopes?

Because `live "/:profile_username"` matches any single-segment path and sits in an authenticated live session, so a `/health` route declared after it would redirect to sign-in. Phoenix matches routes in declaration order, so the `/health` scope goes first and pipes through `:api`, which skips sessions, CSRF and layouts.

### Will the health checks fill the logs?

Phoenix logs each request at info by default, so a check every fifteen seconds adds a few lines a minute. The pull request leaves logging as it is and notes the router and endpoint options that lower the level for these routes if the team wants them quiet.

### Does it change the Fly deployment?

Only the check block in `fly.toml`: the TCP check becomes an HTTP check on `/health/ready` with the same interval, timeout and grace period. The release command, the service ports and the concurrency limits are untouched, and the pull request describes the behavioural difference so the team can keep the TCP check instead if they prefer.
