Skip to content

Commit f717ed9

Browse files
amosttAygentic
andauthored
docs(api): add health endpoints documentation and update test registry [skip ci] (#5)
- Create docs/api/endpoints/health.md with full schema docs for /healthz, /readyz, and /version including Kubernetes probe YAML examples - Update docs/api/overview.md to add "Operational Endpoints" section at root level and mark legacy /utils/health-check/ as superseded - Update docs/testing/test-registry.md: add 17 health integration tests, coverage 188 → 205 total Related to AYG-68 🤖 Generated by Aygentic Co-authored-by: Aygentic <noreply@aygentic.com>
1 parent 1c01590 commit f717ed9

File tree

3 files changed

+293
-9
lines changed

3 files changed

+293
-9
lines changed

docs/api/endpoints/health.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
title: "Operational Endpoints API"
3+
doc-type: reference
4+
status: current
5+
version: "1.0.0"
6+
base-url: "/"
7+
last-updated: 2026-02-28
8+
updated-by: "api-docs-writer (AYG-68)"
9+
related-code:
10+
- backend/app/api/routes/health.py
11+
- backend/app/main.py
12+
- backend/app/core/config.py
13+
related-docs:
14+
- docs/api/overview.md
15+
- docs/architecture/overview.md
16+
tags: [api, rest, health, operations, liveness, readiness, version]
17+
---
18+
19+
# Operational Endpoints API
20+
21+
## Overview
22+
23+
The operational endpoints provide container-orchestrator-compatible probes and
24+
build metadata for this service. They are mounted at the **application root**
25+
(not under `/api/v1`) so that Kubernetes, AWS ECS, and similar platforms can
26+
reach them without API-version routing logic.
27+
28+
**Base URL:** `/` (root — no `/api/v1` prefix)
29+
**Authentication:** None — all three endpoints are fully public
30+
31+
> These endpoints do **not** appear in the `/api/v1/openapi.json` spec and are
32+
> not shown in the Swagger UI at `/docs`. They are intentionally excluded to
33+
> keep the versioned API spec clean.
34+
35+
## Quick Start
36+
37+
```bash
38+
# Liveness probe
39+
curl http://localhost:8000/healthz
40+
41+
# Readiness probe
42+
curl http://localhost:8000/readyz
43+
44+
# Build metadata
45+
curl http://localhost:8000/version
46+
```
47+
48+
---
49+
50+
## Endpoints
51+
52+
### GET /healthz
53+
54+
Liveness probe. Returns `200 OK` immediately with no dependency checks. Use
55+
this endpoint to tell the orchestrator that the process is alive and the event
56+
loop is running. It never contacts Supabase or any other external service.
57+
58+
**Authentication:** None
59+
**Authorization:** Public
60+
61+
**Parameters:** None
62+
63+
**Response (200):**
64+
65+
| Field | Type | Description |
66+
|-------|------|-------------|
67+
| `status` | string | Always `"ok"` |
68+
69+
**Example Request:**
70+
71+
```bash
72+
curl -X GET "http://localhost:8000/healthz"
73+
```
74+
75+
**Example Response:**
76+
77+
```json
78+
{"status": "ok"}
79+
```
80+
81+
**Error Responses:**
82+
83+
This endpoint has no application-level error responses. A non-`200` reply
84+
indicates a process crash or network-level failure, not an API error.
85+
86+
---
87+
88+
### GET /readyz
89+
90+
Readiness probe. Verifies that the service can accept traffic by checking
91+
connectivity to Supabase. Returns `200` when all checks pass; returns `503`
92+
when any dependency is unreachable.
93+
94+
Container orchestrators use the `200`/`503` distinction to gate traffic:
95+
a pod that is alive (`/healthz` = 200) but not ready (`/readyz` = 503) is
96+
kept out of the load-balancer rotation until dependencies recover.
97+
98+
**Authentication:** None
99+
**Authorization:** Public
100+
101+
**Parameters:** None
102+
103+
**Supabase check logic:**
104+
105+
The check issues a lightweight `HEAD` request against a probe table via the
106+
Supabase PostgREST client. It considers the server **reachable** even if the
107+
table does not exist (PostgREST returns an HTTP-level `APIError` in that case,
108+
which still proves connectivity). Only a connection-level failure — or a
109+
missing Supabase client on `app.state` — is treated as `"error"`.
110+
111+
**Response (200 — ready):**
112+
113+
| Field | Type | Description |
114+
|-------|------|-------------|
115+
| `status` | string | `"ready"` |
116+
| `checks` | object | Per-dependency check results |
117+
| `checks.supabase` | string | `"ok"` when Supabase is reachable |
118+
119+
**Example Request:**
120+
121+
```bash
122+
curl -X GET "http://localhost:8000/readyz"
123+
```
124+
125+
**Example Response (200 — ready):**
126+
127+
```json
128+
{
129+
"status": "ready",
130+
"checks": {
131+
"supabase": "ok"
132+
}
133+
}
134+
```
135+
136+
**Example Response (503 — not ready):**
137+
138+
```json
139+
{
140+
"status": "not_ready",
141+
"checks": {
142+
"supabase": "error"
143+
}
144+
}
145+
```
146+
147+
**Error Responses:**
148+
149+
| Status | When |
150+
|--------|------|
151+
| `200 OK` | Supabase is reachable (or PostgREST returned a table-level error, which still proves connectivity) |
152+
| `503 Service Unavailable` | Supabase connection failed, timed out, or `app.state.supabase` is not initialised |
153+
154+
> **Note:** Unlike most API errors, the `503` response from `/readyz` does NOT
155+
> use the [standard error shape](../overview.md#standard-error-responses). It
156+
> returns the plain `{"status": "not_ready", "checks": {...}}` body shown above,
157+
> because this endpoint is designed for machine consumption by orchestrators, not
158+
> API clients.
159+
160+
---
161+
162+
### GET /version
163+
164+
Returns build metadata injected at container image build time via environment
165+
variables. API gateways use this endpoint for service registration and
166+
canary-deployment tracking.
167+
168+
**Authentication:** None
169+
**Authorization:** Public
170+
171+
**Parameters:** None
172+
173+
**Response (200):**
174+
175+
| Field | Type | Description |
176+
|-------|------|-------------|
177+
| `service_name` | string | Service identifier (from `SERVICE_NAME` env var; default `"my-service"`) |
178+
| `version` | string | Semantic version string (from `SERVICE_VERSION` env var; default `"0.1.0"`) |
179+
| `commit` | string | Git commit SHA of the deployed image (from `GIT_COMMIT` env var; default `"unknown"`) |
180+
| `build_time` | string | ISO 8601 timestamp of the image build (from `BUILD_TIME` env var; default `"unknown"`) |
181+
| `environment` | string | Active deployment environment (from `ENVIRONMENT` env var; e.g. `"local"`, `"staging"`, `"production"`) |
182+
183+
**Example Request:**
184+
185+
```bash
186+
curl -X GET "http://localhost:8000/version"
187+
```
188+
189+
**Example Response:**
190+
191+
```json
192+
{
193+
"service_name": "my-service",
194+
"version": "1.2.0",
195+
"commit": "a3f1c2d",
196+
"build_time": "2026-02-28T08:00:00Z",
197+
"environment": "production"
198+
}
199+
```
200+
201+
**Example Response (unset build vars — local development):**
202+
203+
```json
204+
{
205+
"service_name": "my-service",
206+
"version": "0.1.0",
207+
"commit": "unknown",
208+
"build_time": "unknown",
209+
"environment": "local"
210+
}
211+
```
212+
213+
**Error Responses:**
214+
215+
This endpoint has no application-level error responses. It reads from
216+
in-process settings and cannot fail at the application layer.
217+
218+
---
219+
220+
## Kubernetes Probe Configuration
221+
222+
Typical Kubernetes deployment snippet using these endpoints:
223+
224+
```yaml
225+
livenessProbe:
226+
httpGet:
227+
path: /healthz
228+
port: 8000
229+
initialDelaySeconds: 5
230+
periodSeconds: 10
231+
232+
readinessProbe:
233+
httpGet:
234+
path: /readyz
235+
port: 8000
236+
initialDelaySeconds: 10
237+
periodSeconds: 15
238+
failureThreshold: 3
239+
```
240+
241+
---
242+
243+
## Changelog
244+
245+
| Version | Date | Change |
246+
|---------|------|--------|
247+
| 1.0.0 | 2026-02-28 | AYG-68: Initial release — `/healthz`, `/readyz`, `/version` |

docs/api/overview.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
title: "API Overview"
33
doc-type: reference
44
status: draft
5-
version: "1.1.0"
5+
version: "1.2.0"
66
base-url: "/api/v1"
7-
last-updated: 2026-02-27
8-
updated-by: "api-docs-writer (AYG-65)"
7+
last-updated: 2026-02-28
8+
updated-by: "api-docs-writer (AYG-68)"
99
related-code:
1010
- backend/app/main.py
1111
- backend/app/api/main.py
@@ -14,6 +14,7 @@ related-code:
1414
- backend/app/api/routes/users.py
1515
- backend/app/api/routes/items.py
1616
- backend/app/api/routes/utils.py
17+
- backend/app/api/routes/health.py
1718
- backend/app/api/routes/private.py
1819
- backend/app/models.py
1920
- backend/app/core/config.py
@@ -77,7 +78,19 @@ Token expiry is controlled by Clerk session settings. Clients should treat token
7778

7879
## Endpoint Summary
7980

80-
Endpoints are grouped by resource. All paths are relative to the base URL `/api/v1`.
81+
Endpoints are grouped by resource. **Operational endpoints** (`/healthz`, `/readyz`, `/version`) are mounted at the **root level** — they are not under `/api/v1`. All other paths are relative to the base URL `/api/v1`.
82+
83+
### Operational Endpoints (Root Level)
84+
85+
These endpoints are public (no authentication required) and mounted directly on the application root for compatibility with container orchestrators and API gateways. They do not appear in the `/api/v1/openapi.json` spec.
86+
87+
| Method | Path | Description | Auth Required |
88+
|--------|------|-------------|:-------------:|
89+
| `GET` | `/healthz` | Liveness probe — returns `{"status": "ok"}` immediately | No |
90+
| `GET` | `/readyz` | Readiness probe — checks Supabase connectivity | No |
91+
| `GET` | `/version` | Build metadata for API gateway service discovery | No |
92+
93+
> **Note:** `/readyz` returns `200` when all checks pass and `503` when any dependency is unreachable. Container orchestrators (Kubernetes, ECS) use these distinct status codes to gate traffic routing. `/healthz` always returns `200` regardless of dependency state.
8194
8295
### Auth / Login
8396

@@ -124,7 +137,7 @@ Non-superusers can only access items they own. Accessing another user's item ret
124137

125138
| Method | Path | Description | Auth Required | Superuser |
126139
|--------|------|-------------|:-------------:|:---------:|
127-
| `GET` | `/utils/health-check/` | Liveness probe — returns `true` | No | No |
140+
| `GET` | `/utils/health-check/` | Legacy liveness probe — returns `true` (superseded by `/healthz`) | No | No |
128141
| `POST` | `/utils/test-email/` | Send a test email to a given address | Yes | Yes |
129142

130143
### Private (local environment only)
@@ -371,6 +384,7 @@ CORS allowed origins are controlled by two configuration values:
371384

372385
## Endpoint Reference
373386

387+
- [Operational Endpoints — Health, Readiness, Version](endpoints/health.md)
374388
- [Login & Authentication](endpoints/login.md)
375389
- [Users](endpoints/users.md)
376390
- [Items](endpoints/items.md)
@@ -380,5 +394,6 @@ CORS allowed origins are controlled by two configuration values:
380394

381395
| Version | Date | Change |
382396
|---------|------|--------|
397+
| 1.2.0 | 2026-02-28 | AYG-68: Operational endpoints (`/healthz`, `/readyz`, `/version`) added at root level; Utils `/health-check/` marked as legacy |
383398
| 1.1.0 | 2026-02-27 | AYG-65: Auth updated to Clerk JWT; unified error response shape documented; `PaginatedResponse[T]` and `offset`/`limit` pagination params documented |
384399
| 1.0.0 | 2026-02-26 | Initial release |

docs/testing/test-registry.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
title: "Test Registry"
33
doc-type: reference
44
status: draft
5-
last-updated: 2026-02-27
6-
updated-by: "data-model-docs-writer"
5+
last-updated: 2026-02-28
6+
updated-by: "api-docs-writer"
77
related-code:
88
- "backend/tests/**/*.py"
99
- "frontend/tests/**/*.spec.ts"
@@ -18,7 +18,7 @@ tags: [testing, quality, registry]
1818

1919
| Module | Unit | Integration | E2E | Total |
2020
|--------|------|-------------|-----|-------|
21-
| backend/api/routes | 0 | 47 | 0 | 47 |
21+
| backend/api/routes | 0 | 64 | 0 | 64 |
2222
| backend/core/config | 13 | 0 | 0 | 13 |
2323
| backend/core/errors | 20 | 0 | 0 | 20 |
2424
| backend/core/logging | 6 | 0 | 0 | 6 |
@@ -33,7 +33,7 @@ tags: [testing, quality, registry]
3333
| frontend/user-settings | 0 | 0 | 14 | 14 |
3434
| frontend/sign-up | 0 | 0 | 11 | 11 |
3535
| frontend/reset-password | 0 | 0 | 6 | 6 |
36-
| **Total** | **80** | **47** | **61** | **188** |
36+
| **Total** | **80** | **64** | **61** | **205** |
3737

3838
> Unit tests in `backend/tests/unit/` can run without database env vars. The conftest guard pattern in that directory skips DB-dependent fixtures automatically.
3939
@@ -107,6 +107,28 @@ tags: [testing, quality, registry]
107107
|-----------|-------------|------|--------|
108108
| test_create_user | Creates user via private API without auth | integration | passing |
109109

110+
### Backend — Integration: Health (`backend/tests/integration/test_health.py`)
111+
112+
| Test Name | Description | Type | Status |
113+
|-----------|-------------|------|--------|
114+
| test_returns_200_ok | Returns 200 with {"status": "ok"} for liveness check | integration | passing |
115+
| test_no_auth_required (healthz) | Succeeds without Authorization header | integration | passing |
116+
| test_response_schema_exact (healthz) | Response contains only the status field | integration | passing |
117+
| test_never_checks_dependencies | Does not access Supabase client in liveness probe | integration | passing |
118+
| test_healthy_supabase_returns_200 | Returns 200 when Supabase is reachable | integration | passing |
119+
| test_unreachable_supabase_returns_503 | Returns 503 when Supabase connection fails | integration | passing |
120+
| test_api_error_still_reports_ok | Treats PostgREST APIError as server reachable | integration | passing |
121+
| test_missing_supabase_client_returns_503 | Returns 503 when app.state.supabase unset | integration | passing |
122+
| test_exception_does_not_crash | Returns valid JSON 503, not a 500 crash | integration | passing |
123+
| test_no_auth_required (readyz) | Succeeds without Authorization header | integration | passing |
124+
| test_response_schema_exact (readyz) | Response has only status and checks fields | integration | passing |
125+
| test_returns_200_with_metadata | Returns 200 with all five metadata fields | integration | passing |
126+
| test_includes_service_name | Includes service_name for gateway discoverability | integration | passing |
127+
| test_default_values_for_unset_env_vars | GIT_COMMIT and BUILD_TIME default to unknown | integration | passing |
128+
| test_custom_settings_values | Reflects custom settings in response body | integration | passing |
129+
| test_response_schema_exact (version) | Response has exactly five expected fields | integration | passing |
130+
| test_no_auth_required (version) | Succeeds without Authorization header | integration | passing |
131+
110132
### Backend — Unit: Config (`backend/tests/unit/test_config.py`)
111133

112134
| Test Name | Description | Type | Status |

0 commit comments

Comments
 (0)