Pet Health is an AGPL-3.0 self-hostable health timeline for multi-pet households. It uses Rust, Axum, Askama, HTMX, SeaORM, SQLite/WAL, and Rig for optional LLM-backed structured capture.
The first complete workflow is intentionally direct:
An owner can write “Milo vomited just now”, immediately see a timestamped structured event on Milo's timeline, and undo it.
The application also creates expiring, revocable, read-only vet links. Share tokens are stored only as SHA-256 hashes and are displayed to the owner once.
The console keeps a dated weight history for each pet. Blood-test files are stored under a private household directory beneath BLOOD_TESTS_DIR (local default: ./example_blood_tests; production compose default: /persistent/blood_tests). The web console can upload a PDF or image directly, and Syncthing can place files in the matching household directory. The owner then chooses Import new tests, or calls the MCP upload_blood_test / import_blood_tests tools.
Imports use Mistral OCR 4 (mistral-ocr-4-0) with block and table extraction. The OCR text is stored alongside parsed test name, value, unit, reference range, flag, and test date. Spanish and English labels are accepted, and the original OCR text remains available for review when a row cannot be parsed. Set MISTRAL_API_KEY in the environment or local .env; never commit that file.
Pet Health exposes an authenticated MCP endpoint at /mcp. MCP clients can use OAuth with S256 PKCE, discovered from the standard /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server endpoints. Existing Pet Health session tokens also remain supported as revocable bearer tokens. Every tool call is scoped to that account's household.
The available tools cover listing pets, reading a pet timeline, reading care context, adding a pet, recording an observation, and undoing an event. Write tools are explicit, preserve the user's original wording, and let the server choose timestamps.
For Claude Code, add the endpoint and let Claude complete the OAuth consent flow:
claude mcp add --transport http pethealth https://your-host/mcpFor Codex, add the same URL as a remote MCP server. The client can discover the OAuth endpoints and ask the user to approve access. Keep write actions enabled only for users who should be able to change the household record.
If the client is running over SSH without a display, use the device-code fallback: the client posts its PKCE challenge to /oauth/device, prints the returned user_code, and polls /oauth/token with the returned device_code. Open the returned verification_uri on any phone or computer, enter the code, sign in, and approve. The code expires after 10 minutes and can be used once.
cp .env.example .env
set -a
source .env
set +a
cargo runOpen http://localhost:3000. On a new database, APP_USERNAME and APP_PASSWORD create the initial owner account for the default household. They are bootstrap credentials only; after the account exists, password changes happen from Account settings and revoke every active session.
Additional owners can register with an email address. Each registration creates a separate household, and all pet, event, share, and audit queries remain household-scoped. Passwords use salted Argon2 hashes. Browser sessions are random, stored server-side as SHA-256 token hashes, expire after 30 days, and use HttpOnly, SameSite=Lax, and production-only Secure cookies.
When PRODUCTION=true, the database location is not configurable: it resolves to:
/persistent/pethealth.sqlite
The process refuses to start if /persistent is absent or the production password is unchanged. Mount durable local storage at /persistent; SQLite runs in WAL mode with foreign keys, a busy timeout, and synchronous=FULL.
The checked-in compose.production.yml is the Coolify production definition. It deliberately has no host port mapping, pulls the public ghcr.io/dunctk/pethealth:mvp image, and declares the durable volume explicitly:
docker build -t pethealth:mvp .
APP_USERNAME=owner APP_PASSWORD='replace-me' docker compose -f compose.production.yml up -dIn Coolify, route the pethealth service to https://your-host:3000. Keep the bootstrap APP_PASSWORD in Coolify's protected environment settings rather than adding it to the compose file. Back up the named volume containing /persistent/pethealth.sqlite together with its WAL files.
The main branch workflow runs the Rust checks and publishes ghcr.io/dunctk/pethealth:mvp to GitHub Container Registry using the built-in GITHUB_TOKEN. No repository secrets are needed.
After the first successful publish, open the package settings on GitHub and set the pethealth container package visibility to Public. Coolify pulls the image without credentials.
When configured, Rig is the first pass for every natural-language capture, so the record box can structure free-form observations instead of relying on fixed phrases. A small deterministic parser remains as a safe fallback for known phrases when the provider is unavailable or omitted:
OPENROUTER_API_KEY=...
# Optional overrides:
# LLM_BASE_URL=https://openrouter.ai/api/v1
# LLM_MODEL=openai/gpt-5.6-sol
OPENROUTER_API_KEY is the preferred provider-specific name; LLM_API_KEY remains accepted for backwards compatibility. The base URL defaults to OpenRouter and the model defaults to openai/gpt-5.6-sol, so neither override is required. The production compose file passes both key names through to the container. The capture box also uses the selected pet when a note says “she”, “he”, or “they” without repeating the pet's name. A note saying no medication was given and appetite was reasonable is saved as a care update and marks the pet's active prescriptions as missed for that date.
The model proposes a typed event only. Rust resolves the pet, validates the proposal, chooses the timestamp, and performs the tenant-scoped transaction.
cargo fmt --check
cargo check --all-targets
cargo test
docker build -t pethealth:mvp .