# pgpeek full agent context > status: ok > canonical: https://descope-sample-apps.github.io/pgpeek/ > concise context: https://descope-sample-apps.github.io/pgpeek/llms.txt > structured index: https://descope-sample-apps.github.io/pgpeek/agent.json This file is the full-detail escape hatch for agents. Prefer `llms.txt` when the task only needs product identity, startup, safety, or probe behavior. ## Agent interface contract - API errors use an appropriate non-2xx status and JSON body `{"error":""}`. - An empty saved-query collection is the definitive JSON value `[]`. - `GET /healthz` returns JSON build metadata: `status`, semantic `version`, Git `commit`, and `buildDate`. - `GET /readyz` returns plain text `ready` or status 503 with `{"error":"database not ready"}`. - Large query results stop at `PGPEEK_ROW_CAP`; the default is 1000 rows. - The default statement timeout is 30 seconds. - The database role, not the SQL parser, is the primary read-only boundary. ## Precomputed documentation summary - Website sections: 11 - Product features: 6 - Built-in themes: 20 - Read-only enforcement layers: 3 - Website configuration rows: 23 - Documented HTTP routes: 25 --- # pgpeek A minimal, **read-only**, team-shared Postgres browser. Built to replace Adminer for the support team. pgweb-style browsing — a **sidebar of tables/views** you click to page through rows, a **Structure** tab showing each table's columns, and a **SQL** tab for free-form `SELECT`s with saved/preset queries — plus CSV export everywhere. Read-only by design: no row editing, schema management, or migrations. ## What it looks like ``` ┌─ tables ──────┬─ Data │ Structure │ SQL ─────────────┐ │ 🔍 filter… │ id email created_at │ │ public │ 1 a@x.com 2026-01-02… │ │ • users ◀ │ 2 b@y.com 2026-01-03… │ │ • companies │ … │ │ auth │ ◀ Prev 1–100 Next ▶ [Export] │ │ • sessions │ │ └───────────────┴───────────────────────────────────────┘ ``` - **Data** tab — click a table → paged rows (Prev/Next). A global search box (matches any column), per-column filters with operators (`=`, `≠`, `<`, `>`, `ILIKE`, `IS NULL`, …), and click-to-sort headers. Foreign-key cells are **click-through links** that jump to the referenced row. CSV export respects the active search/filters/sort. Wide tables stay usable with sticky headers, clipped cell previews, full values on demand, and mobile overflow containment. - **Structure** tab — column name, type, nullable, default. - **SQL** tab — CodeMirror editor with table/field autocomplete, capped previews, exact row counts, saved/preset queries, and row-uncapped gzip CSV export up to 512 MiB before compression. Filtering is safe by construction: column names are validated against the relation's real columns and emitted via `pgx.Identifier`, operators come from a fixed allowlist, values are bound as query parameters, and sort is `ASC`/`DESC` only — no user input is ever concatenated into SQL. It exists because Adminer kept falling over. pgpeek avoids those failure modes on purpose: - **Connection pooling** (`pgx`/pgxpool) — not a new connection per request. - **Row cap** — results stop at `PGPEEK_ROW_CAP` rows; an enormous result set is never fully buffered into memory. The UI tells you when output was capped. - **Statement timeout** — `statement_timeout` is set on every pooled session, so a runaway query can't wedge a pod. - **Stateless pods** — saved queries live in a small SQLite file on a PVC, not in pod memory. The app process holds no per-user state. ## Architecture ``` browser ── HTTP ──> pgpeek (Go, single static binary) │ pgx pool ──> Postgres (Aurora, read-only role) └ SQLite file ──> saved/preset queries (on a PVC) ``` - **Backend**: Go, `jackc/pgx/v5` for Postgres, `modernc.org/sqlite` (pure Go, no cgo) for the saved-query store → static binary, ~25 MB distroless image. - **Frontend**: one `web/index.html` — CodeMirror editor (CDN, degrades to a textarea), results table, saved-query dropdown, CSV button. Embedded into the binary via `go:embed`. LLM-friendly project notes live in [`llms.txt`](llms.txt); the published docs site serves the same file at `/llms.txt`. ## Read-only enforcement (defense in depth) 1. **The real boundary**: the DB role (`descoperead`) has no write privileges. That's what actually keeps the data safe. 2. **Session-level**: pgpeek sets `default_transaction_read_only=on` on every pooled connection. 3. **App-layer guardrail** (`internal/guard`): rejects anything that isn't a single `SELECT`/`WITH`/`VALUES`/`TABLE`/`EXPLAIN` statement — blocks multiple statements and DML/DDL keywords, ignoring keywords that appear inside comments or string literals. This is a guardrail against fat-fingering, **not** the security boundary. Don't rely on it as one. ## MCP pgpeek exposes a stateless [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) MCP endpoint at `http(s):///mcp`, implemented with the official Go SDK. MCP responses use JSON rather than long-lived SSE sessions, which keeps the endpoint simple to run behind an ingress or load balancer. Request bodies are capped at 32 KiB before protocol parsing. Structured tool output is capped at 448 KiB; query and discovery responses set `truncated=true` when rows or catalog entries are omitted to stay within that budget. The server advertises four structured, read-only tools: | Tool | Purpose | | ------------------ | ------- | | `list_databases` | List safe database IDs and display names. | | `list_tables` | List user-facing tables and views. | | `describe_table` | Return columns and single-column foreign keys. | | `query` | Run one guarded, row-capped read-only SQL statement. | For MCP clients that accept a URL-based server entry: ```json { "mcpServers": { "pgpeek": { "url": "http://localhost:8080/mcp" } } } ``` MCP authentication is optional. With no Descope settings, `/mcp` remains unauthenticated and should stay on a trusted network or behind an existing access proxy. Cross-origin browser requests are rejected, and `PGPEEK_REQUIRE_CLOUDFLARE_ACCESS=true` remains an additional gate when enabled. ### Descope OAuth with Dynamic Client Registration pgpeek can act as an OAuth resource server while Descope handles Dynamic Client Registration (DCR), login, consent, and token issuance: 1. In Descope Agentic Identity Hub, create an MCP Server whose **Server URL** is the full public pgpeek endpoint, for example `https://pgpeek.example.com/mcp`. 2. Enable **Dynamic Client Registration** and define the scope or scopes that should grant access to pgpeek's read-only tools. 3. Copy the MCP Server's OpenID well-known URL and configure all three values: ```sh export DESCOPE_MCP_SERVER_WELL_KNOWN_URL='https://api.descope.com/v1/apps/agentic///.well-known/openid-configuration' export PGPEEK_MCP_SERVER_URL='https://pgpeek.example.com/mcp' export PGPEEK_MCP_REQUIRED_SCOPES='mcp:pgpeek.read' ``` `DESCOPE_CONFIG_URL` is also accepted as an alias for the well-known URL used by Descope's B2B MCP example. If both names are set, they must match. At startup pgpeek fetches the discovery document, verifies that it advertises a DCR `registration_endpoint` and every configured scope, then prepares local verification against its `jwks_uri` using the supported asymmetric signing algorithms advertised by Descope. Each `/mcp` request must present a Bearer token with the discovered issuer, an `aud` matching `PGPEEK_MCP_SERVER_URL`, a valid expiry, and every configured scope. pgpeek does not need a Descope management key or project secret. OAuth clients discover Descope through public RFC 9728 metadata at `/.well-known/oauth-protected-resource`; unauthenticated MCP responses point to that URL in `WWW-Authenticate`. The metadata endpoint remains public when Cloudflare Access enforcement is enabled, while `/mcp` must pass both gates. ## Configuration (env vars) Everything is configured via the environment. Single-database deployments can keep using `DATABASE_URL`; multi-database deployments can use a URL list, numbered env vars, or a mounted JSON config file. Secret-bearing URLs can be supplied from mounted files so they do not live in manifests. | Variable | Default | Notes | | ---------------------------- | -------------------- | --------------------------------------------------------------------- | | `DATABASE_URL` | single-DB required | Postgres DSN for single-database installs. Use the read-only role. **Never logged.** Aurora: include `?sslmode=require`. | | `DATABASE_URL_FILE` | — | Path to a file holding the DSN (mounted-secret alternative). | | `PGPEEK_DATABASE_URLS` | — | Comma- or semicolon-separated DSNs for multiple databases. Quoted CSV values are supported. | | `PGPEEK_DATABASE_IDS` | `db1`, `db2`, … | Optional comma/semicolon IDs matching `PGPEEK_DATABASE_URLS`; URL-safe (`A-Z`, `a-z`, `0-9`, `_`, `-`, `.`). | | `PGPEEK_DATABASE_NAMES` | `Database N` | Optional display names matching `PGPEEK_DATABASE_URLS`. | | `PGPEEK_DATABASE_URL_1` | — | Numbered DSN form. Continue with `_2`, `_3`, …; each also supports `_FILE`. | | `PGPEEK_DATABASE_ID_1` | `db1` | Optional ID for numbered database 1. | | `PGPEEK_DATABASE_NAME_1` | `Database 1` | Optional display name for numbered database 1. | | `PGPEEK_DATABASES_FILE` | — | Path to a mounted JSON config file with database entries. | | `PGPEEK_DEFAULT_DATABASE` | first configured DB | Default database ID when the URL has no `db=` parameter. | | `PGPEEK_LISTEN` | `:8080` | Listen address. | | `PGPEEK_ROW_CAP` | `1000` | Max rows returned to the browser per query. Direct exports are row-uncapped with a 512 MiB raw CSV limit. | | `PGPEEK_CATALOG_LIMIT_BYTES` | `4194304` (4 MiB) | Max encoded size of a table/column/foreign-key catalog listing. Raise this if a schema has enough tables to overflow the default and the UI reports "table catalog exceeds response limit". | | `PGPEEK_STATEMENT_TIMEOUT` | `30s` | Per-query DB statement timeout. | | `PGPEEK_IDLE_TX_TIMEOUT` | `30s` | `idle_in_transaction_session_timeout`. | | `PGPEEK_MAX_CONNS` | `8` | Max pool size (caps DB connection usage). | | `PGPEEK_STORE_PATH` | `/data/pgpeek.db` | SQLite file for saved queries. | | `PGPEEK_READ_HEADER_TIMEOUT` | `10s` | HTTP read-header timeout. | | `PGPEEK_WRITE_TIMEOUT` | `statementTimeout+30s` | HTTP write timeout (must exceed statement timeout for big exports). | | `PGPEEK_IDLE_TIMEOUT` | `120s` | HTTP keep-alive idle timeout. | | `PGPEEK_SHUTDOWN_TIMEOUT` | `15s` | Graceful-shutdown grace period. | | `PGPEEK_TLS_CERT_FILE` | — | Enable HTTPS (set together with the key). Otherwise serve plain HTTP behind a TLS-terminating ingress. | | `PGPEEK_TLS_KEY_FILE` | — | TLS private key path. | | `PGPEEK_REQUIRE_CLOUDFLARE_ACCESS` | `false` | Return 403 unless Cloudflare Access headers are present; probes and OAuth protected-resource metadata stay open. | | `DESCOPE_MCP_SERVER_WELL_KNOWN_URL` | — | Enable Descope OAuth for `/mcp` with this MCP Server OpenID discovery URL. `DESCOPE_CONFIG_URL` is an accepted alias. | | `PGPEEK_MCP_SERVER_URL` | — | Full public MCP URL ending in `/mcp`; used as protected resource and required JWT audience. | | `PGPEEK_MCP_REQUIRED_SCOPES` | — | Comma- or whitespace-separated Descope scopes required on every MCP request. | | `PGPEEK_DB_IAM_AUTH` | `false` | Use RDS/Aurora IAM auth instead of a password (see below). | | `PGPEEK_AWS_REGION` | `$AWS_REGION` | AWS region for IAM token signing (required when IAM auth is on). | ### Multiple databases / clusters The UI compresses the selected database, table, tab, filters, sort, pagination, and SQL editor text into one `?s=...` parameter, so links are bookmarkable and shareable. Older readable URL parameters still open and canonicalize to `s`. Opening a shared SQL link restores the editor without running the query. Packed state is capped at 8 KiB; larger SQL remains editor-only. Same-env list form: ```bash export PGPEEK_DATABASE_URLS='postgres://reader:PASSWORD@prod:5432/app?sslmode=require;postgres://reader:PASSWORD@analytics:5432/warehouse?sslmode=require' export PGPEEK_DATABASE_IDS='prod;analytics' export PGPEEK_DATABASE_NAMES='Production;Analytics' export PGPEEK_DEFAULT_DATABASE=prod ``` Numbered env var form: ```bash export PGPEEK_DATABASE_URL_1_FILE=/run/secrets/prod-url export PGPEEK_DATABASE_ID_1=prod export PGPEEK_DATABASE_NAME_1=Production export PGPEEK_DATABASE_URL_2_FILE=/run/secrets/analytics-url export PGPEEK_DATABASE_ID_2=analytics export PGPEEK_DATABASE_NAME_2=Analytics ``` Mounted config file form (`PGPEEK_DATABASES_FILE=/config/pgpeek/databases.json`): ```json { "default": "prod", "databases": [ { "id": "prod", "name": "Production", "urlFile": "/secrets/prod-url" }, { "id": "analytics", "name": "Analytics", "urlFile": "/secrets/analytics-url" } ] } ``` Kubernetes example (ConfigMap-mounted config + Secret-mounted DSNs; illustrative only, not an extra manifest to commit): ```yaml env: - name: PGPEEK_DATABASES_FILE value: /config/pgpeek/databases.json volumeMounts: - name: pgpeek-db-config mountPath: /config/pgpeek readOnly: true - name: pgpeek-db-urls mountPath: /secrets readOnly: true volumes: - name: pgpeek-db-config configMap: name: pgpeek-db-config - name: pgpeek-db-urls secret: secretName: pgpeek-db-urls ``` Docker Compose example (volume-mounted JSON + secret files; illustrative only): ```yaml services: pgpeek: environment: PGPEEK_DATABASES_FILE: /config/pgpeek/databases.json volumes: - ./pgpeek-config:/config/pgpeek:ro - ./pgpeek-secrets:/secrets:ro ``` ### RDS / Aurora IAM authentication Set `PGPEEK_DB_IAM_AUTH=true` and `PGPEEK_AWS_REGION`. The `DATABASE_URL` then needs only host/port/user/dbname (no password) and `sslmode=require`. pgpeek mints a short-lived IAM auth token from the default AWS credential chain (env / web-identity / **IRSA** / instance role) **before every new connection**, so tokens never go stale and no static DB password is stored anywhere. ```bash export PGPEEK_DB_IAM_AUTH=true export PGPEEK_AWS_REGION=us-east-1 export DATABASE_URL='postgres://descoperead@your-cluster.cluster-xxxx.us-east-1.rds.amazonaws.com:5432/yourdb?sslmode=require' ``` In k8s, attach an IRSA-annotated ServiceAccount (see `k8s/serviceaccount.yaml`) whose role has `rds-db:connect` on the `descoperead` DB user. ## Run locally ```bash export DATABASE_URL='postgres://descoperead:PASSWORD@host:5432/db?sslmode=require' export PGPEEK_STORE_PATH=./pgpeek.db go run . # open http://localhost:8080 ``` Keyboard: **Ctrl/Cmd + Enter** previews the query. ## Build ```bash make build # static binary (CGO disabled) make image # snapshot distroless image via goreleaser + ko docker build -t pgpeek . # alternative: hand-written multi-stage Dockerfile ``` Builds run with Go FIPS 140-3 mode enabled (`GOFIPS140=v1.0.0`, `GODEBUG=fips140=on`). The hand-written Dockerfile uses Verity's Go FIPS builder image and keeps the runtime distroless/nonroot. Release images are built with [ko](https://ko.build) inside goreleaser (distroless, multi-arch, reproducible, with SBOMs) — see [Releases](#releases). ## Testing & quality The backend is at **100% statement coverage** on every package under `internal/` (guard, db, store, server, config, awsauth); the front-end (`web/app.js`) is at **100%** lines/branches/functions via vitest. `package main` is thin bootstrap, exercised by integration tests. ```bash make test # unit tests, race detector make test-integration # + db/main integration tests (needs Postgres) make cover-check # full coverage profile, fail if internal/ < 100% make lint # golangci-lint (errcheck, gosec, revive, …) make vulncheck # govulncheck make web-test # vitest --coverage (100% thresholds) make ci # everything above ``` A throwaway Postgres for integration/coverage: ```bash docker run -d --name pg -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=testdb -p 55432:5432 postgres:16 make cover-check # uses PGPEEK_TEST_DATABASE_URL (default points at :55432) ``` ## Releases - **release-please** watches `main` for [Conventional Commits](https://www.conventionalcommits.org) and maintains a release PR (version bump + `CHANGELOG.md`). Merging it tags `vX.Y.Z` and cuts a GitHub Release. - The tag triggers **goreleaser** (`.goreleaser.yaml`), which builds the binaries and uses **ko** to publish multi-arch distroless images to `ghcr.io/descope-sample-apps/pgpeek:{version,major.minor,latest}` with SBOMs. Release builds pin Go FIPS mode with `GOFIPS140=v1.0.0` and `GODEBUG=fips140=on`. CI (`.github/workflows/ci.yml`) runs lint, vet, race tests with a Postgres service, the 100% coverage gate, govulncheck, the vitest suite, and a snapshot image build on every PR. ## Deploy to k8s Manifests live in [`k8s/`](k8s/): `Deployment`, `Service`, `PersistentVolumeClaim`, an optional `Ingress`, and a `secret.example.yaml`. ```bash # 1. Create the DB secret out-of-band (do NOT commit it): kubectl create secret generic pgpeek-db \ --from-literal=DATABASE_URL='postgres://descoperead:PASSWORD@your-aurora-host:5432/yourdb?sslmode=require' # 2. Apply the rest: kubectl apply -k k8s/ ``` The pod runs as non-root with a read-only root filesystem (only `/data` is writable), drops all capabilities, and has liveness (`/healthz`) and readiness (`/readyz`) probes. The example Deployment also sets `GOFIPS140=v1.0.0` and `GODEBUG=fips140=on` so runtime crypto follows the same FIPS profile as release builds. ### Kubernetes security and secret mounting Production rule: keep DSNs and DB passwords out of Git and out of ConfigMaps. Use a read-only Postgres role, require TLS to the database (`sslmode=require`), and put pgpeek behind SSO before it is reachable by users. Supported secret patterns, from simplest to most flexible: 1. **Single DB, env from Secret** — the shipped `k8s/deployment.yaml` reads `DATABASE_URL` from the `pgpeek-db` Secret. Create that Secret out-of-band with `kubectl create secret`, Sealed Secrets, External Secrets Operator, or your platform's secret manager sync. 2. **Single DB, mounted Secret file** — mount a Secret key as a file and set `DATABASE_URL_FILE=/secrets/database-url`. This keeps the secret out of pod env dumps while still using one database. 3. **Multiple DBs, env from Secret** — set `PGPEEK_DATABASE_URLS` from a Secret when your secret manager already publishes the whole DSN list as one value. Keep `PGPEEK_DATABASE_IDS` and `PGPEEK_DATABASE_NAMES` in a ConfigMap when they are non-secret. 4. **Multiple DBs, numbered Secret files** — mount one Secret file per DSN and set `PGPEEK_DATABASE_URL_1_FILE=/secrets/prod-url`, `PGPEEK_DATABASE_URL_2_FILE=/secrets/analytics-url`, plus matching IDs and display names. 5. **Multiple DBs, ConfigMap + Secret files** — mount non-secret routing config as `PGPEEK_DATABASES_FILE=/config/pgpeek/databases.json`, and keep each database DSN in a Secret-mounted file referenced by `urlFile`. 6. **No static DB password** — for RDS/Aurora, set `PGPEEK_DB_IAM_AUTH=true`, `PGPEEK_AWS_REGION`, use a passwordless DSN, and annotate `k8s/serviceaccount.yaml` for IRSA with `rds-db:connect` on the read-only DB user. Keep app config and secrets separated: ConfigMaps may hold database IDs, names, row caps, and timeouts; Secrets should hold DSNs, TLS private keys, and any auth material. If pgpeek terminates TLS itself, mount certificate/key files from a Secret and set `PGPEEK_TLS_CERT_FILE` + `PGPEEK_TLS_KEY_FILE`; otherwise terminate TLS at the Ingress or mesh. Network/auth checklist: - Do not publish the UI or HTTP API directly to the internet. Use oauth2-proxy, Cloudflare Access/Tunnel, VPN, private ingress, or a service mesh auth layer. - For public MCP access, configure Descope DCR auth and keep `PGPEEK_MCP_SERVER_URL` identical to the Server URL registered in Descope. - If using Cloudflare Access, set `PGPEEK_REQUIRE_CLOUDFLARE_ACCESS=true` only when the origin cannot be reached except through Cloudflare. - Keep public infrastructure endpoints open: `/healthz`, `/readyz`, and `/.well-known/oauth-protected-resource` remain reachable without Cloudflare Access headers. - Restrict egress to the configured Postgres endpoints and any AWS STS/RDS IAM endpoints required for IAM auth. - Rotate DB credentials in the backing Secret; restart pods if your secret sync mechanism does not refresh mounted files/env automatically. ### A note on scaling The saved-query store is a SQLite file on a **ReadWriteOnce** PVC, so the Deployment ships with `replicas: 1` and a `Recreate` strategy. The query path itself is stateless. To scale horizontally, move the saved-query store to a shared backend (a dedicated schema in Postgres, or an RWX volume) and bump `replicas` — see comments in `k8s/pvc.yaml`. ### Auth The UI and JSON API remain intentionally **auth-thin** — put them behind your existing SSO. The example `Ingress` assumes oauth2-proxy (Entra/Google SAML). **Do not expose them without an auth layer in front of them.** The MCP endpoint can additionally validate Descope-issued OAuth tokens when the three MCP auth variables above are set. This does not authenticate the UI or JSON API. Cloudflare Access is detected from `Cf-Access-Authenticated-User-Email` and shown in the UI. Set `PGPEEK_REQUIRE_CLOUDFLARE_ACCESS=true` to reject requests without Cloudflare Access headers. This is not JWT validation; only use it when the origin is reachable only through Cloudflare Access/Tunnel. ## Managing preset queries Two ways: - **From the UI**: write a query, click **Save**. Saved queries appear in the dropdown (grouped "Presets" vs "Saved") and persist in the SQLite store. - **Seeded on first boot**: edit `internal/store/presets.go` and rebuild. These seed only when the store is empty, so they never clobber the team's edits. The shipped presets (custom-domains-per-company, recent signups, table sizes) are illustrative — adjust table/column names to your actual schema. ## Endpoints | Method & path | Purpose | | --------------------------------------------- | ---------------------------------------------- | | `GET /api/databases` | List configured databases with safe runtime details (version, uptime, size, workload/cache/temp/deadlock/session counters, extensions, and connection limits). Core PostgreSQL does not expose portable host CPU, RAM, or free-disk metrics. | | `GET /api/user` | Current detected user (`anonymous` or Cloudflare Access email). | | `POST /api/query?db=` | Run a query → JSON `{columns, rows, …}`. | | `POST /api/query/count?db=` | Run a query as an exact `COUNT(*)` without returning its rows. | | `POST /api/query/cell?db=` | Re-run a query and return one full cell by row/column index. | | `POST /api/export?db=` | Run a query → gzip-compressed CSV download (`.csv.gz`). | | `GET /api/meta?db=` | Server limits the UI needs (`{rowCap}`). | | `GET /api/tables?db=` | List browsable tables/views (+ row estimate). | | `GET /api/tables/{schema}/{table}/columns?db=` | Column structure (name, type, nullable, default). | | `GET /api/tables/{schema}/{table}/fks?db=` | Single-column foreign keys (for click-through). | | `GET /api/tables/{schema}/{table}/data?db=` | Paged rows; `&limit=&offset=&search=&sort=&dir=&f=col:op:val` (`&format=csv`). | | `GET /api/tables/{schema}/{table}/data/cell?db=` | Replay the current page and return one full cell by row/column index. | | `GET /api/queries` | List saved/preset queries. | | `POST /api/queries` | Create a saved query. | | `PUT /api/queries/{id}` | Update a saved query. | | `DELETE /api/queries/{id}` | Delete a saved query. | | `GET /mcp` | Protocol-defined Streamable HTTP transport behavior. | | `POST /mcp` | Send stateless Streamable HTTP MCP messages. | | `DELETE /mcp` | Protocol-defined Streamable HTTP session termination behavior. | | `OPTIONS /mcp` | CORS preflight when Descope OAuth protects the MCP endpoint. | | `GET /.well-known/oauth-protected-resource` | Public OAuth protected-resource metadata when Descope MCP auth is enabled. | | `OPTIONS /.well-known/oauth-protected-resource` | CORS preflight for public OAuth metadata. | | `GET /healthz` | Liveness plus `{status,version,commit,buildDate}` metadata. | | `GET /readyz` | Readiness (pings the DB). | | `GET /` | The UI. |