CaloGraph is a self-hosted nutrition dashboard for Apple Health and YAZIO data. It puts individual days into the context of weekly budgets, trends, micronutrients, and data coverage without moral judgment or external telemetry. CaloGraph currently focuses on nutrition data. Optional activity support is planned, while additional health data may be considered separately in the future.
Warning
Work in progress: CaloGraph is under active development. Version 0.x
releases may contain incomplete features, breaking changes, or migration
issues. Keep tested backups and review the changelog before updating. It is
not medical software and must not be used for diagnosis or treatment.
Important
A server cannot retrieve Apple Health or HealthKit data from iCloud. An authorized iPhone app such as Health Auto Export must send the data to CaloGraph over HTTPS. A historical Apple Health export can alternatively be uploaded as XML or ZIP.
- Daily calories, protein, carbohydrates, and fat
- Micronutrient analysis for 13 vitamins and 13 minerals, including source coverage and a neutral EU NRV comparison
- Versioned nutrition targets and accurate weekly budgets
- 7-, 14-, and 28-day averages without treating missing days as zero
- Weekday analysis with mean, median, and percentiles
- Accessible calendar view with graded calorie-budget deviations
- Clear distinction between recorded and missing nutrition data
- Idempotent REST imports for Health Auto Export v2 and the CaloGraph sync format
- Defensive historical Apple Health XML/ZIP imports
- Experimental YAZIO JSON import, manual retrieval, encrypted scheduled sync, and dashboard-triggered sync
- Local authentication, CSRF protection, and hashed import tokens
- Fully local Docker Compose deployment without third-party analytics
YAZIO → Apple Health on iPhone → iPhone exporter → HTTPS/JSON
↓
Browser → Nginx/Vue (127.0.0.1:8180) → FastAPI → PostgreSQL
Apple Health XML/ZIP ↗
YAZIO days.json ↗
direct YAZIO sync ──↗
Import adapters normalize every source into the same health_samples model.
Analytics and the frontend do not depend on the original export format. See
docs/architecture.md for details.
Requirements: Docker Engine with Docker Compose v2. PostgreSQL and Node do not need to be installed on the host.
cp .env.example .env
scripts/init-secrets.sh
vim .env
docker compose pull
docker compose up -d --no-build --wait
docker compose psThe standard template sets ENVIRONMENT=development and is intended only for
a loopback installation. For Internet-facing operation, start from
.env.production.example. Production startup fails closed when HTTPS, cookie,
secret, database, proxy, YAZIO encryption, or upload-capacity settings are
unsafe or inconsistent.
scripts/init-secrets.sh creates independent database, session, rate-limit,
YAZIO credential-encryption, and MFA-encryption secrets in the ignored
secrets/ directory without printing them. Compose mounts only the specific
file required by each service under /run/secrets; secret values are not
stored in .env or placed in container environments. The database DSN is
assembled in memory from its non-secret connection fields and the mounted
password.
Existing installations that still keep direct secret values and
DATABASE_URL in .env can migrate them once without rotating the database
password or YAZIO key:
CONFIRM_SECRET_MIGRATION=calograph scripts/migrate-env-secrets.shThe migration is fail-closed, never prints a value, and refuses to overwrite an existing secret destination. Back up and inspect the installation before running it. Installations already using the original four secret files add the separate MFA key with:
CONFIRM_MFA_SECRET_MIGRATION=calograph scripts/migrate-mfa-secret.shYAZIO key handling is described in docs/yazio-sync.md.
CALOGRAPH_PUBLIC_URL is the canonical browser address used for links that
leave the application, especially user invitations. Keep the local default for
a loopback-only installation. When using a reverse proxy, set it to the final
HTTPS origin. Reserved example.com, example.net, and example.org
hostnames are documentation placeholders and are rejected in production. The
configured hostname and origin are automatically added to the effective
request allowlists.
Production nevertheless requires both values to be listed explicitly in
TRUSTED_HOSTS and TRUSTED_ORIGINS, making the deployed policy auditable.
The backend container applies pending Alembic migrations before it starts. The application is then available at http://127.0.0.1:8180.
Compose pulls the public latest application images from GHCR by default.
The production template keeps that default; set CALOGRAPH_VERSION to a
reviewed vX.Y.Z release and use --no-build when reproducibility matters.
Contributors can build the checked-out source with make dev or
docker compose up -d --build.
The default docker-compose.yml contains only the four runtime services:
PostgreSQL, backend, YAZIO scheduler, and frontend. Test runners and the
ephemeral test database live in docker-compose.test.yml; the Make targets and
test scripts select that overlay explicitly.
Both application images are built from the central multi-stage
Dockerfile. Compose selects separate backend and frontend
targets, so the final images still contain only their respective runtime. Both
run as non-root users. Backend process settings such as WEB_CONCURRENCY,
UVICORN_LOG_LEVEL, and the Uvicorn timeouts can be overridden in .env
without rebuilding the image. Build labels and the backend UID/GID can
optionally be overridden with CALOGRAPH_VERSION, CALOGRAPH_UID, and
CALOGRAPH_GID.
GitHub CI builds, scans, tests, signs, and publishes release images to GHCR.
Every successful release tag publishes both its vX.Y.Z tag and latest.
Override the image names only when using a mirror or fork.
The immutable pins, SBOMs, provenance verification, and retention policy are
documented in docs/supply-chain.md.
docker compose exec backend python -m app.cli create-userThe command prompts for a username and password. Initial passwords must contain at least 15 characters and must not occur in CaloGraph's bundled common-password blocklist. The first user becomes an administrator automatically and can create one-time invitation links under Konto → Benutzerverwaltung.
Each user can optionally enable TOTP under Konto → Zwei-Faktor-Authentifizierung. CaloGraph shows ten one-time recovery codes during activation. Users can also enroll one or more passkeys under Konto → Passkeys and then sign in passwordlessly with their device's biometric check or PIN. Passkeys require HTTPS, except for browser-recognized localhost development.
An operator can recover an account after verifying the user out of band:
docker compose exec backend python -m app.cli issue-account-recovery \
--username USERNAME --admin-username ADMIN
docker compose exec backend python -m app.cli reset-authenticators \
--username USERNAME --admin-username ADMIN --confirm USERNAMEBoth commands require fresh administrator reauthentication. Recovery replaces the password through a one-time token but keeps the account inactive until an administrator explicitly reactivates it. Authenticator reset is available only for an inactive account and removes TOTP, recovery codes, passkeys, sessions, and API tokens without changing the password or reactivating the account.
docker compose exec backend python -m app.cli create-import-tokenThe token is displayed once and stored only as an HMAC-SHA-256 hash. It can be revoked later under Konto → Import-Tokens.
Create a REST API automation in Health Auto Export with these settings:
- Data type: Health Metrics
- Metrics: nutrition only; do not select activity, steps, hydration, or weight
- Format: JSON, Export Version 2
- Summary: disabled where the data volume permits
- Range: previous seven days, so delayed changes are sent again
- URL:
https://your-host.example/api/v1/import/apple-health - Header:
Authorization: Bearer cg_YOUR_TOKEN - Optional:
X-Client-Identifier: my-iphone
Example request using deliberately fake data:
curl --fail-with-body \
--request POST \
--header 'Authorization: Bearer cg_FAKE_EXAMPLE_TOKEN_DO_NOT_USE' \
--header 'Content-Type: application/json' \
--header 'X-Client-Identifier: example-iphone' \
--data-binary @examples/health-auto-export-v2.json \
https://calograph.example/api/v1/import/apple-healthPayloads can be validated without storing them:
curl --fail-with-body \
--request POST \
--header 'Authorization: Bearer cg_FAKE_EXAMPLE_TOKEN_DO_NOT_USE' \
--header 'Content-Type: application/json' \
--data-binary @examples/health-auto-export-v2.json \
https://calograph.example/api/v1/import/apple-health/validateThe source-neutral format intended for a future native iOS app is documented in docs/import-api.md.
In Apple Health, select Profile → Export All Health Data. Upload the
resulting ZIP or its export.xml file under Importe. CaloGraph validates
file sizes, paths, compression ratios, and XML safety. Parsing is streamed, and
repeated imports do not create duplicates. Apple Health uploads are limited to
500 MiB by default. Accepted values are persisted in batches of 500 instead of
being materialized as one large in-memory import.
If a large import stops after one or more committed batches, its status is
partial_failed (Teilweise importiert in the interface). The completed
batches remain available; uploading the same file again safely continues the
idempotent import without duplicating those values.
A completely synthetic example is available at
examples/apple-health-export.xml.
Apple Health supplies measurements and their sources, but often does not include reliable food, recipe, or meal names. CaloGraph therefore does not require them.
CaloGraph can import days.json and nutrients.json files produced by
yazio-exporter from
the Importe page. Calories, macronutrients, and supported micronutrients are
aggregated per day. Product, recipe, and meal names are not stored. Direct sync
retrieves 13 vitamins and 13 minerals through the exporter's separate nutrient
endpoints.
A manual direct import retrieves the previous 60 days by default:
docker compose exec backend python -m app.cli sync-yazio \
--username admin \
--email name@example.comThe password is requested without echo. Passwords and access tokens remain in
memory only for the duration of this command. Use
--from-date YYYY-MM-DD --end-date YYYY-MM-DD for a historical range; one
request is limited to 366 days.
This integration relies on an undocumented YAZIO interface and may stop working
after provider-side changes. Health Auto Export remains the recommended
default. Do not import the same days from Apple Health and directly from YAZIO,
because separate sources are intentionally not deduplicated across source
boundaries. Explicit request and operation deadlines, per-user and per-IP rate
limits, PostgreSQL-backed concurrency control, a temporary circuit breaker, and
the YAZIO_ENABLED kill switch protect direct access without requiring Redis.
See docs/yazio-sync.md.
After creating a user, generate 120 completely synthetic days:
docker compose exec backend python -m app.cli seed-demo-data --username adminThe seed includes weekend patterns, missing days, varied intake, a target change, and the daylight-saving-time transition in Berlin. It never runs automatically.
make dev
make test
make lint
make typecheck
make frontend-test
make build
make e2eAlternatively, run the tools inside their respective directories:
cd backend
uv sync --frozen --all-extras
uv run pytest
uv run ruff check app tests
uv run mypy app
cd ../frontend
npm ci
npm run lint
npm run typecheck
npm run test:unit
npm run buildThe development template enables OpenAPI documentation at /api/docs and
/api/openapi.json. Production disables both endpoints by default through
ENABLE_API_DOCS=false; they can be enabled deliberately for a restricted
operator or API-client network.
The regular backend test suite always replaces an inherited DATABASE_URL
with an in-memory SQLite database before importing the application. PostgreSQL
integration tests are isolated behind ./scripts/test-postgres.sh; their
destructive schema reset requires an explicit opt-in and a local database name
ending in _test.
export BACKUP_AGE_RECIPIENTS_FILE=/etc/calograph/backup-recipients.txt
BACKUP_DIR=/srv/calograph-backups \
BACKUP_SECRETS=1 \
scripts/update-containers.shThe script first creates an age-encrypted database backup without a
plaintext temporary dump. BACKUP_SECRETS=1 additionally encrypts .env and
the service-scoped files under secrets/. Key generation, off-host storage,
verification, and restore instructions are in
docs/backup-restore.md.
The backend refuses to start when migrations fail.
- Health values and payloads are excluded from normal logs.
- Raw JSON payload retention is disabled by default
(
RAW_PAYLOAD_RETENTION_DAYS=0); large XML/ZIP uploads are not duplicated in the database. - PostgreSQL has no host port mapping.
- The frontend binds to
127.0.0.1:8180by default. ENVIRONMENTis mandatory.productionrequires an HTTPS public URL, secure cookies, HSTS, non-default independent secrets, and exact request allowlists before the backend or scheduler starts.- TLS terminates at a reverse proxy. Never expose a configuration using
ENVIRONMENT=developmentthrough that proxy. - Forwarded headers are accepted only from the fixed frontend proxy IP.
Compose derives an exact
/32fromCALOGRAPH_FRONTEND_PROXY_IP, and production rejects subnet-wide trust. - No CDN dependencies, telemetry, external analytics, or third-party data transfer.
See SECURITY.md, docs/threat-model.md, and docs/reverse-proxy.md.
- Architecture
- Data model
- Import API
- Apple Health and exporter setup
- Analytics definitions
- Threat model
- Backup and restore
- Production Docker operation
- User management
- Reverse proxy
- Software supply chain
- Future native iOS sync
Contributions are welcome. Please read CONTRIBUTING.md and the project-specific Code of Conduct before opening a pull request. Report vulnerabilities privately according to SECURITY.md.
CaloGraph's original code and assets are licensed under the PolyForm Noncommercial License 1.0.0. You may use, modify, and redistribute them for noncommercial purposes; commercial use is not permitted. This is a source-available license, not an OSI-approved open-source license.
External dependencies retain their own licenses. In particular,
yazio-exporter is a separately maintained MIT-licensed dependency and is not
CaloGraph-owned code. See THIRD_PARTY_NOTICES.md.
The original CaloGraph logos are stored in
frontend/public/branding:
calograph-logo-long.png— horizontal logocalograph-app-logo.png— app and store logocalograph-icon.png— isolated color iconcalograph-logo-monochrome.png— monochrome variant
The frontend uses an optimized 256-pixel app image and a theme-independent README banner derived from these originals.
- The dashboard supports German and English. Public authentication pages are always English; authenticated users can store their dashboard language in the Konto profile.
- No native iOS app and no access to a supposed Apple Health cloud API.
- Scheduled YAZIO sync uses an undocumented interface and may break after changes made by YAZIO.
- No CSV importer.
- No meal analysis without corresponding source data.
- No medical diagnosis or automated nutrition advice.
- Horizontal multi-host deployment and Kubernetes are not planned.

