Fetch the Open-Meteo air-quality feed (CAMS, domains=auto) with the weather: 17 hourly variables folded into local days by fixed rules (means and maxima only with 18 of 24 hours, ozone as the highest 8-hour running mean, UV only with the midday hours), a value the feed did not cover stored as null, never as zero. Requests are cut into 90-day chunks, newest first, and chunks before mid 2022 ask for the eight pollutant and index variables only. An air-quality failure never costs the weather row; the day stays unfetched and the nightly gap fill (up to 90 days per account, newest first) asks for it again. Every Open-Meteo request asks an instance-wide budget kept in Postgres first (500 a minute, 4000 an hour, 8000 a day, weighted by the hosted pricing rule). A refused request is not sent and the run ends as done with budget_blocked. OPENMETEO_AIR_QUALITY_URL and ENVIRONMENT_AIR_QUALITY_DISABLED are honoured; the account switch lives on PATCH /api/environment/preferences. The home, each dated location period and each stored day keep their coarse location sealed as one value under healthlog/environment-location/v1. The writers write the sealed value only, the readers open it and fall back to the readable columns for rows the free-text encryption backfill has not reached, and the backfill now seals all three and clears the readable columns. The backup carries the location readable in a portable file and sealed in a disaster-recovery file; a restore seals either under the receiving key. Three correlation channels join the environment family (PM2.5, the ozone 8-hour high, the day's highest pollen count), read through the averaged same-day window and left empty while air quality is off. The Coach and MCP gain get_environment: module-gated, clamped to the lookback limit, no coordinate or place name anywhere, with provenance and the attribution lines. The settings section gets the switch, the coverage and the newest day's values; the dashboard shows a quiet chip on a day with high pollen, a warm night or very poor air. Copernicus and Open-Meteo are credited next to the values.
HealthLog API Spec
openapi.yaml is the OpenAPI 3.1 description for the HealthLog
Next.js API and the source of truth for the native iOS client's DTO
codegen. The file is generated: pnpm openapi:generate emits it
from the Zod registry under src/lib/openapi/ (registry.ts plus the
route modules in src/lib/openapi/routes/). Do not edit the YAML by
hand; the next generation overwrites it, and CI (openapi:check)
fails the build when the committed YAML drifts from the registry.
The MCP server is documented separately — it is a distinct contract over JSON-RPC, not part of the REST OpenAPI spec:
mcp-capabilities.md— the tool, resource, and prompt catalogue, the grounding contract, and the write model.mcp-skills.md— building a connector or skill on top of it.../self-hosting/mcp.md— enabling and connecting the surface (operator guide).
Preview locally
Open an interactive Redoc preview in your browser:
npx @redocly/cli preview docs/api/openapi.yaml
Generate static HTML for sharing:
npx @redocly/cli build-docs docs/api/openapi.yaml --output docs/api/index.html
Validate
npx @redocly/cli lint docs/api/openapi.yaml
The spec is expected to be lint-clean (errors = 0). Warnings such as
no-server-example.com for the localhost dev server are tolerated;
re-run the lint to see what it produces against the current file
before treating a warning as a regression.
Layout
info— title, description, andversionmirrored automatically frompackage.jsonat generation time. Never bump it by hand.servers—https://healthlog.example.comas the self-hosted placeholder plushttp://localhost:3000for local development.tags— domain groupings; the current set lives at the top of the generated file (Auth,Measurements,Medications,Mood,Analytics,Insights,Consent,Dashboard,Notifications,Devices,Export,Sync,Cycle,MeasurementReminders,Labs,Illness,Documents,Records,Admin,Meta,Retired).securitySchemes:-
bearerAuth—hlk_*API tokens. A scope grants what it names and nothing adjacent, so where a token comes from decides what it reaches:- a completed password / passkey / refresh exchange mints the
cookie-equivalent
["*"]token the native client carries; - a medication's own API-endpoint toggle mints
medication:ingestplusmedication:<id>:ingest, the pair/api/ingest/medicationgates on; POST /api/mcp/tokensmintshealth:read(optionally plushealth:write), audience-bound to/mcpand refused on REST writes;POST /api/tokens/measurementsmintsmeasurements:write, accepted byPOST /api/measurementsandPOST /api/measurements/batchonly, on the holder's own record only;POST /api/tokens/workoutsmintsworkouts:write, accepted byPOST /api/workouts/batchonly, on the holder's own record only; the workouts it writes carrysource: EXTERNAL;POST /api/tokens/documentsmintsdocuments:write, accepted byPOST /api/documents/inboundandGET /api/documents/inbound/sourceonly.
POST /api/tokensis not among them — that generic mint was removed in v1.30.17. The path still lists (GET) and revokes (DELETE /api/tokens/{id}) every token above, whichever surface issued it. - a completed password / passkey / refresh exchange mints the
cookie-equivalent
-
sessionCookie— HttpOnlyhealthlog_sessioncookie set by the password and passkey login flows. Used by the web app and any iOS client running inside a web view.
-
components.schemas— DTOs for the documented routes plus the underlying Prisma models (e.g.Measurement,Medication,MedicationIntakeEvent,MoodEntry,User,DashboardLayout,ApiEnvelope,ApiError).paths— the client-facing REST surface. Not everysrc/app/api/**/route.tsis in the contract: most/api/admin/*routes, the OAuth connect/callback legs, and the webhook surfaces are not documented here. TheRetiredtag lists removed paths that answer410 Gone.
Conventions
- Successful responses use the envelope
{ data, error: null }. - Error responses use the envelope
{ data: null, error: '<message>' }. - Pagination uses
limit+offsetquery parameters and ameta: { total, limit, offset }block on the response. - All timestamps are ISO 8601 / RFC 3339 (
format: date-time) in UTC. - IDs are CUIDs (opaque strings).
Updating the spec
When adding or changing routes:
- Update the relevant route handler under
src/app/api/**/route.ts. - Change the Zod schemas and their
.meta()registrations undersrc/lib/openapi/(registry entry plus the matching module insrc/lib/openapi/routes/). KeepoperationIdcamelCase and stable; the iOS codegen keys off it. - Run
pnpm openapi:generateand commit the regeneratedopenapi.yamlalongside the schema change. CI'sopenapi:checkfails on drift. - Never edit the YAML directly and never bump
info.versionby hand; the version followspackage.jsonat generation time.