Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,575 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Schautrack

Build Status GitHub Release License Docker Pulls

Schautrack is a self-hostable, open-source nutrition tracker — log calories, macros, and weight, set goals, and share progress with friends.

Try it: schautrack.com

Schautrack on desktop and mobile

Goals

Tracking nutrition is tedious. But it helps you reach your goals.

Schautrack is built to stay out of your way. Log calories and macros, set goals, and let AI estimate from photos. Simple as that.

Features

  • Log calories and macros (protein, carbs, fat, fiber, sugar)
  • Saved foods for one-tap quick-add of frequent meals
  • Daily goals with color-coded progress tracking
  • AI-powered nutrition estimation from food photos (OpenAI, Claude, or Ollama)
  • Barcode scanning via OpenFoodFacts
  • Weight tracking with unit preference (kg/lbs)
  • Optional body-fat percentage per weigh-in — lean/fat mass, category bands, and a Katch-McArdle calorie budget that beats the height/age estimate
  • Weight-loss planner: set a goal weight, get a personalized calorie budget (BMR/TDEE), timeline projection, and progress tracking
  • Daily notes and recurring todos with streak tracking
  • Account linking to share data with friends — granular, opt-in, read-only per category (nutrition, weight, todos, daily notes)
  • Two-factor authentication (TOTP) with backup codes
  • Brute-force protection with CAPTCHA challenges on login, registration, and verification-email resend
  • Invite-only registration mode
  • Welcome tour on first login, replayable any time from Settings
  • Real-time updates via Server-Sent Events (SSE)
  • Public REST API with scoped personal access tokens (docs)
  • Docker and Kubernetes ready (~21MB image)
  • Android app on Google Play

Android App

Get it on Google Play

Source code available at schautrack-android.

Android App Links

The server serves a Digital Asset Links file at /.well-known/assetlinks.json so that https:// links to your domain open directly in the app (App Links verification; also used for TWA validation).

The endpoint stays disabled (404) until you provide your app's SHA-256 signing-certificate fingerprint(s), which are specific to your build and must not be copied from elsewhere:

  • ANDROID_CERT_FINGERPRINTS — comma-separated fingerprint(s) in AA:BB:...:FF form. Find them in the Play Console under Test and release → App integrity → App signing, or from your keystore with keytool -list -v -keystore <keystore> -alias <alias>.
  • ANDROID_PACKAGE_NAME — defaults to to.schauer.schautrack; override for a fork with a different published package.

Quickstart (Docker)

mkdir schautrack && cd schautrack
curl -O https://raw.githubusercontent.com/schaurian/schautrack/main/compose.yml
curl -O https://raw.githubusercontent.com/schaurian/schautrack/main/.env.example
mv .env.example .env
sed -i "s/please-change-me/$(openssl rand -hex 32)/" .env
docker compose up -d

App is available at http://localhost:8080.

Upgrading from an earlier compose.yml? The database volume now mounts at /var/lib/postgresql instead of /var/lib/postgresql/data (required for the PostgreSQL 18 image to persist data). If your db container is still running with data, back it up before pulling the new compose.yml:

docker compose exec -T db pg_dumpall -U "$POSTGRES_USER" > schautrack-backup.sql
# update compose.yml, recreate the stack, then restore:
docker compose up -d
cat schautrack-backup.sql | docker compose exec -T db psql -U "$POSTGRES_USER"

Kubernetes (Helm)

A Helm chart is available for Kubernetes deployments with bundled PostgreSQL.

helm repo add schautrack https://helm.schautrack.com
helm repo update
helm install schautrack schautrack/schautrack \
  --set postgresql.auth.password="$(openssl rand -base64 16)"

See Helm Chart Documentation for Ingress, TLS, external databases, AI configuration, and all parameters.

Development Setup

To build from source instead of using pre-built images:

git clone https://github.com/schaurian/schautrack.git
cd schautrack
cp .env.example .env
docker compose -f compose.dev.yml up --build

Environment Variables

Settings follow a strict priority hierarchy: environment variables > admin panel (/admin) > user preferences. When a higher-priority source sets a value, lower-priority sources are ignored and their UI controls are disabled.

Required

Variable Default Description
DATABASE_URL (empty) PostgreSQL connection string (e.g. postgresql://user:pass@host:5432/db)

General

Variable Default Description
PORT 3000 Port to listen on
ADMIN_EMAIL (empty) Email that gets access to /admin page
SUPPORT_EMAIL (empty) Contact email shown on support/error pages
BASE_URL (auto-detect) Base URL for SEO meta tags (e.g., https://schautrack.com). Auto-detects from request if not set.

AI Features

Photo-based nutrition estimation with support for OpenAI, Claude, and Ollama.

Configuration priority: Environment variables > admin panel settings > user settings. When any global AI config is set (provider or key), user personal AI settings are ignored. Users can only bring their own API key when no global config exists.

Variable Default Description
AI_PROVIDER (empty) AI provider to use: openai, claude, or ollama. Required to enable AI features.
AI_KEY (empty) Global API key (used by all users; overrides personal keys)
AI_KEY_ENCRYPTION_SECRET (empty) Random 32-byte hex string for encrypting user API keys
AI_ENDPOINT (empty) Custom endpoint override (e.g., http://your-ollama-host:11434/v1). Leave blank to use provider defaults.
AI_MODEL (empty) Specify AI model to use (e.g., gpt-4o, claude-sonnet-4-5-20250929, gemma3:12b). Required for OpenAI and Claude.
AI_DAILY_LIMIT (unset → unlimited) Daily limit for AI requests per user when using global key (0 or unset = unlimited). The app applies no limit unless this is set via env or the admin panel. Note: the Helm chart sets this to 10 by default.

Note: Ollama models must be downloaded before use. The docker-compose setup automatically pulls the model specified in AI_MODEL. Models specified only in API requests will fail if not pre-downloaded.

SMTP (Transactional Email)

Configuring SMTP enables all transactional email flows: password reset, registration email verification, email-change verification, and 2FA reset. Without SMTP configured, none of these flows can deliver their codes.

Variable Default Description
SMTP_HOST (empty) SMTP server hostname
SMTP_PORT 587 SMTP port
SMTP_USER (empty) SMTP username
SMTP_PASS (empty) SMTP password
SMTP_FROM SUPPORT_EMAIL From address for emails
SMTP_SECURE false Set to true for SSL/TLS

Features

Variable Default Description
ENABLE_BARCODE true Enable barcode scanning via OpenFoodFacts. Set false to disable.
ENABLE_REGISTRATION open open (anyone can register) or false / invite (requires invite code). Also configurable via /admin.
UPDATE_CHECK_ENABLED true Check GitHub (api.github.com) for a newer release so the footer can flag an outdated instance. Set false to opt out of the outbound request — recommended for privacy-sensitive or air-gapped self-hosts.
UPDATE_PROVIDER github Which forge hosts the release feed the check reads. github or gitlab.
UPDATE_REPO schaurian/schautrack The owner/name the check asks about. Point it at your own fork to track your releases rather than upstream's.
UPDATE_BASE_URL (empty) API base for a self-hosted forge, e.g. https://gitlab.example.com/api/v4. Empty means the provider's public API.

OIDC (Single Sign-On)

Generic OpenID Connect — works with Google, Microsoft, GitHub, GitLab, Apple, Keycloak, Authentik, Authelia, Zitadel, or any OIDC-compliant provider. Users are auto-created on first login and auto-linked by email to existing accounts.

Variable Default Description
OIDC_ISSUER (empty) OIDC issuer URL (e.g., https://accounts.google.com). Enables OIDC when set.
OIDC_CLIENT_ID (empty) OAuth2 client ID
OIDC_CLIENT_SECRET (empty) OAuth2 client secret
OIDC_LABEL (derived) Button label. Defaults to a capitalized brand name inferred from the issuer host.
OIDC_REQUIRE_INVITE false Require invite code for OIDC registration (default: OIDC bypasses invite-only)
OIDC_REDIRECT_URL (auto) Callback URL. Auto-built as <BASE_URL>/auth/oidc/callback when unset.

A logo is auto-selected from a small bundled set (Google, Microsoft, GitHub, GitLab, Apple, Keycloak, Authentik, Authelia, Zitadel) when the issuer URL contains the brand name. Otherwise the button shows text only.

Example (Google):

OIDC_ISSUER=https://accounts.google.com
OIDC_CLIENT_ID=123456.apps.googleusercontent.com
OIDC_CLIENT_SECRET=GOCSPX-...

Add https://<your-domain>/auth/oidc/callback as an authorized redirect URI in your provider's OAuth client.

Passkeys

WebAuthn-based passwordless login with biometric verification. Users can register up to 10 passkeys and use them as their primary login method. Passkeys skip 2FA since they are inherently multi-factor.

Variable Default Description
PASSKEYS_RP_ID (empty) Relying Party ID — your domain (e.g., schautrack.com). Enables passkeys when set.
PASSKEYS_RP_NAME Schautrack Display name shown in passkey dialogs
PASSKEYS_RP_ORIGINS https://<RP_ID> Allowed origins (comma-separated, for multi-domain setups)

Security

Variable Default Description
TRUST_PROXY true Trust X-Forwarded-For / X-Real-Ip headers for rate limiting. Set false for direct-access deployments without a reverse proxy.
RATE_LIMIT_AUTH 10 Max authentication attempts per 15 minutes per IP
RATE_LIMIT_STRICT 5 Max requests per 5-minute window per IP on sensitive endpoints (password-reset request/confirm, 2FA reset, email-change request, AI estimate). Also caps POST /api/v1/ai/estimate, there per account — otherwise a token would be a 60x cheaper route to the same paid provider than a browser.
RATE_LIMIT_API 120 Max requests per minute per IP on the public API (/api/v1). The outer guard: it is the only limit that can throttle an unauthenticated flood. Set well above the auth limiters — a script syncing a day of entries makes dozens of calls in a burst.
RATE_LIMIT_API_TOKEN 60 Max requests per minute per token on /api/v1. The limit that matters for a legitimate client: per-IP alone means everyone behind one CGNAT shares a bucket. Both limits return 429 with Retry-After. The two endpoints that cost real resources — POST /ai/estimate and GET /barcode/{code} — are additionally capped per account at the app's own rates (RATE_LIMIT_STRICT and RATE_LIMIT_BARCODE); minting more tokens does not raise those.
RATE_LIMIT_BARCODE 30 Max barcode lookups per minute, applied per IP on the app's own route and per account on GET /api/v1/barcode/{code}. One value for both so the public API cannot become the cheaper path to the same third-party database. Raise it if your users hit 429 while scanning.
LOGIN_CAPTCHA_GLOBAL_THRESHOLD 3 Failed-login count per account email or client IP (cross-session, 15-minute window) at which login demands a captcha. The per-session threshold stays fixed at 3. Raise only in test harnesses where all clients share one IP (read in internal/handler/login_failures.go, outside the config package).
STEP_UP_TTL 30m Grace window after fresh primary auth during which sensitive auth-method changes (delete passkey, disable 2FA, change password/email, etc.) are accepted without re-prompting. Any time.ParseDuration value.

CAPTCHA: A self-generated SVG CAPTCHA guards against brute-force and abuse. It is always required to complete registration and to resend a verification email, and is triggered on login after 3 failed attempts (counted per session, per account, and per client IP). No third-party CAPTCHA service or key is needed — it works out of the box.

CAPTCHA_BYPASS (default: unset) — a test-only escape hatch. When set to true, VerifyCaptcha accepts any non-empty answer, disabling CAPTCHA protection entirely. It exists so the end-to-end test suite (compose.test.yml) can drive auth flows headlessly. Never set this in production — doing so removes all brute-force protection from login and registration.

Sessions: there is no session signing or encryption key, and none is needed. Sessions are server-side — the cookie carries only a session ID of 32 bytes from crypto/rand, and all session data lives in the session table. Authenticity is "this row exists", so there is nothing to sign and no key to rotate. (A signed-cookie design needs one because the cookie carries the session data itself; this one does not.) The cookie is HttpOnly, SameSite=Lax, and Secure whenever the request arrives over TLS or with X-Forwarded-Proto: https.

Consequently, restarting the app or changing configuration does not log anyone out. Sessions are invalidated by deleting their rows, which the app does automatically on every credential change — password reset and change, disabling or resetting 2FA, email change, and account deletion. A login additionally issues a fresh session ID and deletes the old row, so a session fixed before login cannot survive it. To end every session for an account out of band, delete its rows directly:

DELETE FROM "session" WHERE (sess::jsonb->>'userId')::int = <user_id>;

Earlier releases required a SESSION_SECRET environment variable. It was a leftover from the Node backend, where express-session used it to HMAC-sign the session cookie; the Go rewrite never read it. It has been removed, and setting it now has no effect. The Helm chart still accepts config.sessionSecret so existing values files keep validating, but ignores it.

Legal Pages

Variable Default Description
ENABLE_LEGAL false Set to true to enable /imprint, /privacy, /terms
IMPRINT_URL /imprint URL for imprint link
IMPRINT_ADDRESS (empty) Full name and address (use \n for line breaks)
IMPRINT_EMAIL (empty) Contact email (rendered as SVG for spam protection)

SEO / Deployment

Variable Default Description
ROBOTS_INDEX false Set to true to allow search engine indexing (default: noindex for self-hosters)
ANDROID_PACKAGE_NAME to.schauer.schautrack Package name published in /.well-known/assetlinks.json for Android App Links.
ANDROID_CERT_FINGERPRINTS (empty) Comma-separated SHA-256 signing-cert fingerprint(s) (UPPER:CO:LON form) for App Links. Deployment-specific — see Android App Links. Empty disables /.well-known/assetlinks.json.

Public API

Schautrack has a versioned public API at /api/v1 for scripts and integrations — logging a meal from a shortcut, pushing readings from a smart scale, pulling your data into a dashboard.

  • Reference: GET /api/v1/docs — rendered by every instance from its own spec, no token needed. Also generated as docs/api-v1.md.
  • Machine-readable: GET /api/v1/openapi.json (OpenAPI 3.1, no token needed) — point Scalar, Bruno, Insomnia, or openapi-generator at it. Also committed at api/openapi.json.

Create a token under Settings → Account → API tokens. Tokens are scoped, can be given an expiry, and can be revoked at any time. The secret is shown once.

The examples below use schautrack.com; on your own instance use your own host. A running server puts its URL in the OpenAPI document's servers entry (from BASE_URL), so tools that read the served spec — Swagger UI's "Try it out" included — send your token to your instance and nowhere else. The https://schautrack.com/problems/… URIs in error responses are the exception: they are stable identifiers, not endpoints, and are the same on every instance on purpose so clients can branch on them.

curl -H "Authorization: Bearer stk_…" https://schautrack.com/api/v1/me

curl -X POST https://schautrack.com/api/v1/entries \
  -H "Authorization: Bearer stk_…" \
  -H "Content-Type: application/json" \
  -d '{"calories": 450, "name": "Porridge", "protein_g": 12}'

Errors are RFC 9457 problem details. Session cookies are not accepted on /api/v1, which is why it needs no CSRF token.

Retrying a POST is safe if you send an Idempotency-Key — a string you generate once per logical operation and reuse on retry. The first request executes and its response is stored; retries replay it instead of logging the meal twice:

curl -X POST https://schautrack.com/api/v1/entries \
  -H "Authorization: Bearer stk_…" \
  -H "Idempotency-Key: breakfast-2026-08-05" \
  -H "Content-Type: application/json" \
  -d '{"calories": 450, "name": "Porridge"}'

Both artifacts are generated from internal/openapi by go run ./cmd/apidocs, and go test ./... fails if they drift from the code — including if a route exists that the spec does not document, or vice versa.

Architecture

Schautrack runs as a single static Go binary that serves both the JSON API and the compiled React bundle, with PostgreSQL as its only required dependency.

See docs/architecture.md for diagrams of the system context, the request pipeline, how authorization guards compose, how realtime updates fan out across replicas, and the data model.

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Support

Schautrack is free and self-hostable, and it stays that way. If it's useful to you, you can support its development via GitHub Sponsors or PayPal.

License

This project is licensed under the GNU Affero General Public License v3.0.

About

Self-hostable, open-source nutrition tracker — log calories, macros, and weight, set goals, and share progress with friends.

Resources

Contributing

Security policy

Stars

23 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages