Macro Tracker is a phone-first macro tracking app for the day-to-day work of eating like you meant to. It is built around the stuff I actually want when I am logging food: fast daily entries, planned meals, reusable meals and days, barcode scanning, recipes, weight tracking, and enough stats to see patterns without turning breakfast into a spreadsheet ceremony.
Current app version: v3.01
The live app is here: macro.safasfly.dev
Sign in with Google, set your goals during onboarding, and start logging. The hosted version is the easiest way to poke around before deciding whether you want to run your own copy.
- Log meals against a daily macro target, including eaten, planned, and skipped items.
- Scan barcodes and save products so repeat foods get faster over time.
- Estimate a meal from a photo when the AI helper is configured.
- Save reusable foods, meals, full-day templates, and recipes.
- Plan a day ahead, then turn planned meals into real logged meals.
- Track weight and body-fat notes alongside your food log.
- Review summaries, trends, rolling averages, adherence, records, and top foods.
- Use a scoped personal access token API for user-owned data.
- Install it as a PWA and use it comfortably from a phone.
- Moderate shared barcode data and audit changes from the owner/admin tools.
Macro Tracker runs as two services: a Rust backend that owns database access and a Next.js frontend that calls it. The backend requires PostgreSQL; file: and memory: database URLs are not supported for the app runtime.
Requirements:
- Node.js 20+
- pnpm 10+
- A PostgreSQL database
- A Google sign-in flow through Shoo, which is the auth broker this app uses
Clone and install:
git clone https://github.com/lnieuwenhuis/marco-tracker.git macro-tracker
cd macro-tracker
pnpm installExport the required environment variables in the shell or deployment environment for each service. Use the same BACKEND_INTERNAL_SECRET value for the backend and frontend.
export APP_URL=http://localhost:3000
export SESSION_SECRET=change-this-to-a-long-random-string
export BACKEND_INTERNAL_SECRET=change-this-to-another-long-random-string
export BACKEND_URL=http://127.0.0.1:4000
export DATABASE_URL=postgres://macro:macro@localhost:5432/macro_trackerRequired runtime variables:
| Variable | Service | Use |
|---|---|---|
DATABASE_URL |
Backend and migration commands | PostgreSQL connection string for the Rust backend database. Must be postgres:// or postgresql://, not file: or memory:. |
APP_URL |
Backend and frontend | Public URL of the web app, for example http://localhost:3000 locally. |
SESSION_SECRET |
Backend and frontend | Long random secret used for sessions. Required in every environment, including local development — there is no built-in fallback. |
BACKEND_INTERNAL_SECRET |
Backend and frontend | Shared secret the frontend sends when calling backend internal routes. |
BACKEND_URL |
Frontend | URL the Next.js app uses to reach the Rust backend. Defaults to http://127.0.0.1:4000 outside production, but set it explicitly in deployments. |
The web package runs from apps/web, and its production start script reads process.env directly. A repo-root .env file is not loaded automatically by the start command.
Run migrations before starting the backend, then start the backend and web app as separate processes:
# terminal 1: apply PostgreSQL migrations
pnpm db:migrate
# terminal 2: start the Rust backend on port 4000
pnpm backend:start
# terminal 3: start the Next.js web app on port 3000
pnpm devFor production, build the release backend and frontend, then start the two services independently:
# build artifacts before starting services
pnpm backend:build
pnpm build
# service 1
pnpm backend:start:release
# service 2
pnpm --filter @macro-tracker/web startFor a deployed instance, set APP_URL to the public URL, BACKEND_URL to the backend service URL reachable from the frontend server, and use real random values for SESSION_SECRET and BACKEND_INTERNAL_SECRET. If you use remote PostgreSQL, DATABASE_URL uses TLS with certificate verification by default when sslmode is omitted or set to verify-full; use sslmode=require only when your provider requires encrypted TLS without certificate verification.
The production build uses Next.js standalone output and starts that smaller server automatically when it is present. PostgreSQL pools default to a small personal-instance footprint of 3 connections; set POSTGRES_POOL_MAX if you need a different cap.
Macro Tracker API v1 is available under /api/v1/*. Create personal access tokens from /settings/api, then send them as Authorization: Bearer <token>. Tokens start with mtk_v1_, are shown only once, store only a hash in the database, and can be scoped to read or write daily logs, foods, templates, recipes, weight, goals, and stats.
OpenAPI JSON is available at /api/v1/openapi.json, and the readable docs page is /docs/api. API responses use { "ok": true, "data": ... } for success and { "ok": false, "error": { "code": "...", "message": "..." } } for failures. Public API dates use YYYY-MM-DD.
Self-hosted instances need the latest database migrations so the api_tokens table exists before users create tokens.
Useful optional environment variables:
| Variable | Use |
|---|---|
APP_TRUSTED_ORIGINS |
Extra comma-separated origins that are allowed during auth flows. |
SHOO_BASE_URL |
Alternate Shoo base URL. Defaults to https://shoo.dev. Must also be set in the build environment: the Content-Security-Policy is baked in at build time and has to allow the browser to reach this origin for the sign-in token exchange. |
ADMIN_OWNER_EMAILS |
Comma-separated emails that should get owner-level admin access. |
POSTGRES_POOL_MAX |
Optional PostgreSQL pool cap. Defaults to 3 for small deployments. |
POSTGRES_POOL_IDLE_TIMEOUT_MS |
Optional idle timeout for pooled PostgreSQL clients. Defaults to 10000. |
POSTGRES_POOL_CONNECTION_TIMEOUT_MS |
Optional PostgreSQL connection timeout. Defaults to 5000. |
NEXT_CACHE_MAX_MEMORY_MB |
Optional Next.js in-memory cache cap in MB. Defaults to 0, which disables the in-memory data cache entirely; set a non-zero value where cached fetches are expected to hit. |
OPENROUTER_API_KEY |
Enables food-photo estimates via OpenRouter. |
OPENROUTER_MODEL |
Optional primary OpenRouter model. Must be free, for example google/gemma-4-26b-a4b-it:free. |
OPENROUTER_FALLBACK_MODELS |
Optional comma-separated free fallback models. |
OPENROUTER_MODEL_TIMEOUT_MS |
Optional per-model attempt timeout for food-photo estimates (applies to OpenRouter and the AI gateway). |
AI_GATEWAY_URL |
Optional OpenAI-compatible chat-completions URL that replaces OpenRouter for food-photo estimates, for example http://cliproxyapi.railway.internal:8317/v1/chat/completions. Must be https unless the host is loopback or *.railway.internal. See infra/cliproxyapi/. Set on the backend service, and also on the web service (URL only) so the admin benchmark page shows the right models. |
AI_GATEWAY_API_KEY |
Backend-only bearer key for the AI gateway. Required when AI_GATEWAY_URL is set. |
AI_GATEWAY_MODELS |
Optional comma-separated gateway model list. Defaults to gpt-5.6-luna(low),gpt-5.6-luna(medium); the effort suffix is translated by CLIProxyAPI into the reasoning-effort parameter. |
ENABLE_TEST_ROUTES |
Enables controlled test-only routes. Leave off in production unless you are doing a controlled test run. |
TEST_ROUTES_SECRET |
Required whenever ENABLE_TEST_ROUTES=true; send it in the x-test-route-secret header. |
BACKEND_ENABLE_TEST_ROUTES |
Backend-side counterpart to ENABLE_TEST_ROUTES. Enables the test-only role-assignment RPC that Playwright uses. Never set this on a deployed backend. |
This is a pnpm workspace:
apps/web- the Next.js app and PWAapps/backend- the Rust backend servicepackages/db- database schema, migrations, query layer, and database tests
Local development needs PostgreSQL plus the backend and frontend processes:
pnpm install
pnpm db:migrate
# terminal 1
pnpm backend:start
# terminal 2
pnpm devRun pnpm backend:start and pnpm dev in separate terminals so both services stay up while you work.
Useful checks. Use a dedicated PostgreSQL test database whose name clearly contains test, tests, e2e, or ci; destructive test setup refuses plain local app databases like macro_tracker by default. Point the Rust backend and JS test helpers at the same database for the check you are running so backend-backed routes and direct Drizzle assertions share state:
export TEST_DATABASE_URL="postgres://postgres:***@127.0.0.1:55432/macro_tracker_test"
export E2E_DATABASE_URL="postgres://postgres:***@127.0.0.1:55432/macro_tracker_e2e"
# Unit/integration checks use TEST_DATABASE_URL.
export DATABASE_URL="$TEST_DATABASE_URL"
pnpm db:migrate
# terminal 1: keep the backend running against $TEST_DATABASE_URL
pnpm backend:start
# terminal 2: run non-E2E checks
pnpm --filter @macro-tracker/db test
pnpm --filter @macro-tracker/web test
pnpm --filter @macro-tracker/web lint
pnpm typecheck
pnpm --filter @macro-tracker/web exec tsc --noEmit
pnpm --filter @macro-tracker/db exec tsc --noEmit
# E2E uses E2E_DATABASE_URL. Restart the backend against the same database
# before Playwright so global setup, the frontend, and the Rust backend share state.
DATABASE_URL="$E2E_DATABASE_URL" pnpm db:migrate
# terminal 1: keep the backend running against $E2E_DATABASE_URL
DATABASE_URL="$E2E_DATABASE_URL" pnpm backend:start
# terminal 2: run Playwright against that backend/database
DATABASE_URL="$E2E_DATABASE_URL" pnpm test:e2eDatabase helpers:
pnpm db:generate
pnpm db:migrate
pnpm db:studioMigrations run forward only — there are no down-migrations, and deploy applies
them as a Railway preDeployCommand. Once a migration has dropped a table the
data is recoverable only from a backup, so any migration that drops or rewrites
data follows this runbook:
-
Prefer deprecation over
DROP. Rename to_deprecated_<name>and keep it for one release. That turns a rollback into a rename instead of a restore.0010is the precedent for what to avoid: it backfills and then drops in the same migration, which is forward-safe but leaves no way back. -
Take a verified backup immediately before deploying. Verified means restored into a scratch database and checked, not just written:
pg_dump "$DATABASE_URL" --format=custom --file=pre-migration.dump createdb macro_tracker_restore_check pg_restore --dbname=macro_tracker_restore_check pre-migration.dump psql macro_tracker_restore_check -c "select count(*) from users;"
-
Record the migration tag you are moving from, so the rollback target is unambiguous:
psql "$DATABASE_URL" -c "select * from drizzle.__drizzle_migrations order by created_at desc limit 5;"
-
To roll back, restore the dump into a fresh database and repoint
DATABASE_URL; do not attempt to hand-reverse a dropped table in place. -
Deploy the backend first. It refuses to serve before migrations apply (there is a CI test for this), so a failed migration fails closed rather than serving against a half-migrated schema.
Macro Tracker is MIT licensed. See LICENSE.