calibrate is a responsive calorie tracker (desktop + mobile web) for people who want to lose (or maintain) weight by logging food and weight, then comparing daily intake against an estimated target based on their profile. If you self-host, your data stays in your own database.
- Hosted instance: https://calibratehealth.app
- Stack: React + TypeScript + Vite (MUI), Node.js + TypeScript + Express, Postgres (Prisma)
- Installable PWA: add it to your home screen / desktop like an app
Note: calibrate is not medical advice.
- Multi-user accounts (email/password)
- Profile-driven calorie math (Mifflin-St Jeor BMR, activity-based TDEE, fixed daily deficit targets)
- Daily food logging with fixed meal categories (manual entry + food database search)
- Food search + barcode scanning (FatSecret default; USDA optional with API key; Open Food Facts fallback)
- My Foods library + recipe builder for reusable entries
- Lose It CSV import (food logs + weigh-ins)
- Weight logging + trend visualization
- Goal projection from a steady deficit
If you enable the FatSecret provider, you must comply with the FatSecret Platform API Terms of Use and Attribution Policy. The app includes the required "Powered by fatsecret" link on the landing page and above food search results when FatSecret is the active provider; keep that link intact. Review the FatSecret platform docs for the latest terms and attribution requirements: https://platform.fatsecret.com/docs/guides
- Weight trend model:
docs/weight-trend-model.md - Deployment (Compose self-hosting):
deploy/README.md - AWS infra (ECS Fargate):
infra/README.md - Frontend dev/build/PWA:
frontend/README.md
Two supported paths:
For a production-oriented Compose stack (Caddy for HTTPS + reverse proxy, plus the app container), use the files in
deploy/.
Quick start:
-
Build the production image:
docker build -f Dockerfile.app -t calibrate:local . -
Create
deploy/Caddyfileanddeploy/.env:-
Start from a template Caddyfile:
deploy/Caddyfile.prod(production)deploy/Caddyfile.staging(basic auth, handy for private/staging installs)
-
Copy it to
deploy/Caddyfileand edit the site address (replacecalibratehealth.appwith your domain). -
Create a
.envfile with your image + database + session secret:APP_IMAGE=calibrate:local DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DB?schema=public # Generate with: openssl rand -base64 32 SESSION_SECRET=replace-with-random # Optional (recommended): CADDY_EMAIL=you@example.com
-
-
Start the stack:
cd deploy docker compose up -d -
Optional: apply DB migrations explicitly (first deploy, or after pulling changes with new migrations). The production image runs
npm run db:migrateon startup, but running it manually lets you control timing:docker compose exec app npm run db:migrate
Notes:
- Caddy needs ports 80/443 reachable from the internet (and DNS pointing at the machine) to get HTTPS certificates.
- This stack expects you to provide a Postgres database (managed or self-hosted).
- If you want Postgres in Docker on the same machine, add a Postgres service to
deploy/docker-compose.yml(see the repo rootdocker-compose.ymlfor a dev example).
See deploy/README.md for required env vars, staging options, and notes.
The infra/ folder contains the Terraform + GitHub Actions workflow used to deploy calibratehealth.app
(ECS Fargate + ALB, RDS Postgres, Route 53, and deploys via GitHub Actions).
It is a good starting point for deploying to your own AWS account, but it is opinionated and you'll likely want to
customize names/domains to match your setup.
High-level steps:
- Install prereqs: Terraform + AWS CLI (and an AWS account to deploy into).
- Run
terraform applyininfra/bootstraponce to create shared resources (state, IAM/OIDC, ECR, Route 53). - Run
terraform applyininfra/envs/stagingand/orinfra/envs/prodto create the app environment(s). - Populate the required Secrets Manager JSON and configure the GitHub Actions secrets for CI/CD.
Full guide (including required secrets and exact commands): infra/README.md
The devcontainer starts quickly and does not install app dependencies or reset the database automatically. Run
npm run setup inside the container when you need the full app environment; it installs dependencies, generates the
Prisma client, applies migrations, and seeds deterministic dev data.
This repo supports a repo-local .env file (gitignored) for devcontainer secrets. Start by copying .env.example to
.env, then rebuild the devcontainer so .devcontainer/.env is regenerated and Docker can pass the values into the
container.
Devcontainer CLI helpers (worktree-friendly, safe to run from any worktree):
- Start or reuse a container:
npm run devcontainer:up -- <branch|path> - Start a fresh container:
npm run devcontainer:up:new -- <branch|path> - Open a shell in the workspace:
npm run devcontainer:shell -- <branch|path> - Recreate then shell:
npm run devcontainer:shell:new -- <branch|path>
If you omit <branch|path>, the current directory is used. You can pass a worktree branch name (e.g. alpha, beta)
or a path. For example: npm run devcontainer:shell -- alpha or
npm run devcontainer:shell -- --path /home/matthew/code/calibrate-health-alpha.
Codex app local environments are defined under .codex/environments/. The local-devcontainer environment calls the
tracked setup script at .codex/local-environment.setup.mjs:
node .codex/local-environment.setup.mjsThe script targets CODEX_WORKTREE_PATH when the Codex app provides it, installs the repo-local Dev Containers CLI if
needed into a host-level tool cache, copies the source checkout's ignored .env into the new worktree when needed, and
starts that worktree's devcontainer with isolated Compose services and ports. It intentionally does not install app
dependencies, run devcontainer post-create hooks, migrate the database, or seed data; those steps are exposed as Codex
actions.
For Codex-managed worktrees whose folder is still named calibrate-health, the devcontainer identity is derived from
the full worktree path so concurrent app-created worktrees do not share a Compose project, database volume, or dev ports.
Backend and frontend node_modules are mounted as shared lockfile-hashed Docker volumes, so sibling worktrees with the
same lockfiles can reuse installed dependencies without writing them into the Windows bind mount.
Recommended Codex app actions:
- Setup:
npm run codex:setup-app - Recreate Devcontainer:
npm run codex:devcontainer:recreate - Stop Devcontainer:
npm run codex:devcontainer:down - Migrate DB:
npm run codex:db:migrate - Reset DB:
npm run codex:db:reset - Dev server with test-user auto-login:
npm run codex:dev - Storybook component workbench:
npm run codex:storybook - Test:
npm run codex:test - Full CI:
npm run codex:ci - Shell:
npm run codex:shell
The Dev action runs the same setup checks as Setup first, but cached dependencies, already-applied migrations, and
existing seed data are skipped. After the preflight passes, it starts npm run dev:test.
Use Recreate Devcontainer after changing devcontainer config or when the worktree's container state needs a clean
replacement. It tears down the generated Compose stack for the current worktree before starting a fresh container.
Use Stop Devcontainer to remove the current worktree's generated devcontainer stack during cleanup.
When using VS Code with a Codex-created worktree container, use Dev Containers: Attach to Running Container and open
/workspaces/calibrate-health inside the container. Reopen in Container follows VS Code's own devcontainer flow and
may create or recreate a separate container.
- Start the app:
npm run dev - Frontend:
http://localhost:5173(proxies/authand/apito the backend) - Backend/API:
http://localhost:3000 - Dev dashboard (dev-only):
http://localhost:5173/dev(compare providers + test barcode scanning) - Component workbench:
npm run dev:storybook(local Storybook defaults tohttp://localhost:6006; devcontainer worktrees use the generatedSTORYBOOK_PORTin.devcontainer/.env)
The backend supports multiple food search providers. During devcontainer initialization, the repo writes
FOOD_DATA_PROVIDER into .devcontainer/.env from explicit config first, then from available credentials:
FatSecret when both FatSecret credentials are set, USDA when USDA_API_KEY is set, and USDA with api.data.gov's
public DEMO_KEY when no provider credentials are available. This keeps local search usable when Open Food Facts
anonymous access is throttled.
To use FatSecret, set FOOD_DATA_PROVIDER=fatsecret with FATSECRET_CLIENT_ID and FATSECRET_CLIENT_SECRET (either in
the host environment or in the repo-local .env) before the devcontainer is created/rebuilt. During devcontainer
initialization we copy the provider config into .devcontainer/.env (gitignored), and docker compose uses it to pass
the values into the container.
Example (host machine):
export FATSECRET_CLIENT_ID="your-client-id"
export FATSECRET_CLIENT_SECRET="your-client-secret"If you add/change the credentials, rebuild the devcontainer so the generated .devcontainer/.env is refreshed.
To use USDA with your own quota, set FOOD_DATA_PROVIDER=usda and supply USDA_API_KEY before rebuilding the
devcontainer. To test Open Food Facts specifically, set FOOD_DATA_PROVIDER=openfoodfacts; that path depends on the
public Open Food Facts API allowing anonymous requests.
The Codex app runs outside the devcontainer and uses the repo-local actions above to execute commands inside it. The devcontainer does not install or configure the Codex CLI by default, which keeps new worktree startup focused on creating the app container quickly.
The seed script creates a deterministic local test account (test@calibratehealth.app). To speed up onboarding
iterations you can auto-login this user and reset its onboarding state:
- Start with auto-login enabled:
npm run dev:test(setsAUTO_LOGIN_TEST_USER=true) - Reset the test user onboarding state:
npm run dev:reset-test-user-onboarding
Prereqs: Node.js 20.19+ or 22.12+, npm, and a Postgres database.
- Set env vars (recommended via
backend/.env):DATABASE_URL=postgresql://user:password@localhost:5432/fitness_app?schema=publicSESSION_SECRET=some-secretPORT=3000(optional)FOOD_DATA_PROVIDER=fatsecret(optional; defaults by available credentials in the devcontainer)FATSECRET_CLIENT_ID=your-client-id(required whenFOOD_DATA_PROVIDER=fatsecret)FATSECRET_CLIENT_SECRET=your-client-secret(required whenFOOD_DATA_PROVIDER=fatsecret)USDA_API_KEY=your-usda-key(required whenFOOD_DATA_PROVIDER=usda)
- Install deps + generate Prisma client:
npm run setup - Create tables (apply migrations):
npm run db:migrate - Start the app:
npm run dev
If you see Prisma errors like "The table public.User does not exist", you haven't applied migrations yet - run
npm run db:migrate.
npm run dev: runs backend + frontend together (backend/+frontend/) withVITE_ENABLE_SW_DEV=1for local PWA/push validation.npm run dev:test: same asnpm run dev, but auto-logs in the seeded dev user.npm run dev:reset-test-user-onboarding: reset the dev test user to pre-onboarding.npm run dev:backend: runs only the backend (http://localhost:3000).npm run dev:frontend: runs only the frontend (http://localhost:5173).npm run dev:storybook: runs Storybook for isolated React component development. Local runs default tohttp://localhost:6006; devcontainer worktrees use the generatedSTORYBOOK_PORTso concurrent worktrees do not collide.npm run setup: installs deps, runsprisma generate, applies migrations, and seeds dev data when missing.npm run db:migrate: applies committed migrations (use for fresh DBs, CI, and prod).npm test: runs backend unit tests (Node.js test runner).
More:
npm run setup:deps: install backend/frontend dependencies using shared devcontainer caches when available.npm run db:migrate:dev: apply migrations and seed dev data when missing.npm run db:migrate:create: create/apply new Prisma migrations during local development.npm run db:reset:dev: destructive reset for a disposable devcontainer/worktree DB, then seed it.npm run db:reset: destructive reset (drops data and recreates schema).npm run db:seed: seed deterministic dev data (test user + sample logs).npm --prefix backend run db:push:reset: dev-only schema reset usingprisma db push(fast, skips migrations).npm run db:studio: Prisma Studio (DB browser).npm run build: build the frontend.npm run build:storybook: build the static Storybook.npm run lint: lint the frontend.npm run ci:local: run the local equivalent of PR CI (backend build, frontend build, frontend lint, backend tests).npm run test:coverage: print coverage + writebackend/coverage/index.html.
The repo root docker-compose.yml is a development stack that starts postgres, backend, and frontend with live
reload and dev-only defaults.
docker compose up --buildThe frontend is configured as a Progressive Web App (PWA), so it can be installed on desktop/mobile and added to a home screen.
- For local push/PWA flow validation in dev mode, run
npm run dev(service worker enabled by default in local dev scripts). - Test locally:
npm --prefix frontend run build && npm --prefix frontend run previewthen openhttp://localhost:4173and use the browser install UI. - iOS: open the app in Safari and use Share -> Add to Home Screen.
Push notes:
- Browser push registration and delivery require backend VAPID env vars:
WEB_PUSH_PUBLIC_KEY,WEB_PUSH_PRIVATE_KEY, andWEB_PUSH_SUBJECT. - In the devcontainer workflow,
.devcontainer/init-devcontainer-env.mjsauto-generates missing VAPID keys and writes them into.devcontainer/.envduring container initialization. - For local backend runs outside the devcontainer (or for plain
docker compose), setWEB_PUSH_*values explicitly (see.env.exampleandbackend/.env.example).
Production is hosted at https://calibratehealth.app and staging is hosted at https://staging.calibratehealth.app.
This app is configured for a "single origin" deployment where the frontend and backend share the same host. In that setup you do not need CORS, and cookies should remain host-scoped.
Recommended backend env vars:
NODE_ENV=production(orstagingfor staging)DATABASE_URL=...SESSION_SECRET=...(use a different value for staging vs prod)FRONTEND_DIST_DIR=...when the backend should serve the built SPA (the production Docker image sets this already)
Notes:
- Leave
CORS_ORIGINSunset for single-origin prod/staging deployments. - Leave
SESSION_COOKIE_DOMAINunset so prod and staging sessions do not collide in the same browser. - Keep
SESSION_SECRETstable within an environment so sessions remain valid across deploys. SESSION_COOKIE_SECUREdefaults totruein production and staging; override only if you are intentionally serving over plain HTTP.