Skip to content

Run it locally

Last reviewed against the codebase: 2026-07-25.

This page takes you from a fresh clone to a running stack - Postgres, the API server, and the app - entirely on your machine, with no AWS and no cloud credentials. The local stack talks only to a Docker Postgres and (optionally) public data feeds. Waking the hosted environment is a separate, cost-bearing operation covered in Run it in production.

Mental model - what actually runs

Before the commands, know the shape of what you’re starting. Locally there are three long-lived processes plus one optional one, and they are independent:

ProcessCommand surfacePortNeeded for a normal dev loop?
Postgres/PostGISdocker compose5432Yes - everything reads/writes here
API server@workspace/api-server dev3000Yes - serves the app + runs ingest schedulers
App (Expo web)@workspace/firepath-ai web8081Yes - the UI you open in a browser
Redis + wind-workerdocker compose / worker-main6379No - terrain-wind simulation only; dark by default (see below)

The API server is more than an HTTP endpoint: on boot it also starts the ingest schedulers (FIRMS, DEA Hotspots, OpenMeteo), so a locally-running API will populate your Postgres with real Australian data over the first few minutes even if you never touch the UI. That’s why the dashboard has live fires and a wind layer on a clean database - you didn’t seed them, the schedulers fetched them.

1. Install dependencies

Terminal window
corepack enable # activates the pinned pnpm (11.0.9)
cd Fire-Path-AI
pnpm install

corepack enable is not optional cosmetics: this is a pnpm workspace pinned via packageManager: pnpm@11.0.9, and installing with a different pnpm - or with npm/yarn - can resolve a different dependency graph. (A preinstall hook deletes any stray package-lock.json/yarn.lock for exactly this reason.)

Expected: pnpm links every package under artifacts/* and lib/* into a single node_modules. The first install is slow because of a deliberate supply-chain guard - minimumReleaseAge: 1440 in pnpm-workspace.yaml refuses any npm version published less than 24 h ago.

See Prerequisites & access for the exact tool versions this repo is pinned to.

2. Start Postgres (+ optional Redis)

Postgres/PostGIS and Redis are defined in the root docker-compose.yml (postgis/postgis:16-3.4 and redis:7-alpine). Start from the repo root:

Terminal window
docker compose up -d postgres # PostGIS on :5432
docker compose up -d redis # OPTIONAL - wind-worker queue only

The container names are firepath-postgres and firepath-redis, and each has a Compose healthcheck, so you can wait for readiness deterministically instead of guessing:

Terminal window
docker compose ps # STATUS should read "healthy", not just "running"
docker logs firepath-postgres | tail -n 5

Why PostGIS specifically, not plain Postgres? Fire spread, hotspot clustering, and per-asset distance all run as spatial SQL (ST_ClusterDBSCAN, ST_DWithin, geometry columns). The vanilla postgres image lacks the PostGIS extension and the app’s boot migration will fail. Use the image as pinned.

3. Configure the environment

Copy the example env file. Its dev-only defaults already point at the Docker Postgres above, so the stack works with zero edits:

Terminal window
cd Fire-Path-AI
cp .env.example .env

The values that matter locally (the full annotated list is the Configuration reference):

  • DATABASE_URL - pre-set to the docker-compose Postgres (postgresql://firepath:…@localhost:5432/firepath). Match this exactly to the credentials in docker-compose.yml if you change either.
  • JWT_SECRET - any non-empty dev string. If it’s blank the API boots but every authenticated request fails; this is the single most common “why is login broken locally” cause.
  • ENABLE_TEST_LOGINS=true - enables a one-tap local sign-in endpoint (POST /auth/test-login) so you don’t need real OAuth to click around. It hard-404s unless NODE_ENV !== production and this flag is "true", so it can never be reached on a deployed service. Never set it in prod/staging.

Third-party keys can stay blank. FIRMS, Google/Microsoft OAuth, Resend, Twilio, VAPID, and the AI-summary key all degrade gracefully when unset - the relevant route returns 503 or the call becomes a logged no-op, and the rest of the app is unaffected. You do not need any of them for local development. (A blank NASA_FIRMS_API_KEY simply means the FIRMS scheduler fetches nothing; DEA Hotspots and OpenMeteo need no key and will still populate the map.)

4. Apply the database schema

The API server self-migrates a production database on boot, but locally the schema is created with Drizzle push (a diff-and-apply, no migration journal):

Terminal window
pnpm --filter @workspace/db run push

Expected: drizzle-kit connects using DATABASE_URL, prints the tables it is creating/altering, and exits 0. If it stalls, it’s almost always Postgres not being healthy yet (step 2) or a DATABASE_URL mismatch.

5. Run the API server

Terminal window
cd Fire-Path-AI
set -a; source .env; set +a # export the .env vars into this shell
pnpm --filter @workspace/api-server run dev

The dev script sets NODE_ENV=development, builds with esbuild, then starts dist/index.mjs on :3000. The source .env line is required because the dev script reads config from process env, not by auto-loading the .env file - skip it and the server falls back to defaults (or fails on a missing JWT_SECRET).

Verify it’s actually serving before wiring up the UI - the two health endpoints answer different questions:

Terminal window
curl -s localhost:3000/healthz # liveness: always {"status":"ok"} if the process is up
curl -s localhost:3000/readyz # readiness: pings the DB + reports ingest freshness

/healthz is intentionally dependency-free (the orchestrator uses it, and a transient DB blip must not get the task killed). /readyz is the one that tells the truth: it returns 503 with "db":"down" if Postgres is unreachable, and lists each ingest source with its age and a stale flag. On a just-started local stack the ingest list is empty or stale: true until the first fetch cycle completes - that’s expected, not a fault.

See Verify for the full check suite (typecheck, vitest, the optional k6 smoke).

6. Run the app (web)

In a second terminal:

Terminal window
cd Fire-Path-AI
pnpm --filter @workspace/firepath-ai run web # Expo web on :8081

Open http://localhost:8081. With ENABLE_TEST_LOGINS=true the dashboard auto-signs-in on first fetch and pulls real Australian data (FIRMS + DEA Hotspots + OpenMeteo) from your local Postgres. If the map is empty for the first minute, the ingest schedulers simply haven’t completed a cycle yet - leave the API running and it fills in.

For a device or simulator instead of the browser, swap the last word:

Terminal window
pnpm --filter @workspace/firepath-ai run start # Expo dev menu (QR for Expo Go)
pnpm --filter @workspace/firepath-ai run ios # iOS simulator
pnpm --filter @workspace/firepath-ai run android # Android emulator

7. (Optional) Run the wind-worker

Terrain-shaped wind simulation (WindNinja) runs in a separate process from the API - worker-main.ts, the same entrypoint the ECS wind-worker task uses in production. It drains a BullMQ/Redis queue that the API enqueues into.

By default this is completely dark locally, and that is intentional. The queue only activates when REDIS_URL is set (isQueueEnabled() checks for a non-empty REDIS_URL), and REDIS_URL is not in .env.example. So even if you started the Redis container in step 2, no jobs are enqueued and no worker runs until you opt in by adding, e.g.:

Terminal window
# in .env - only if you want to exercise terrain wind locally
REDIS_URL=redis://localhost:6379

Stop everything

  • Dev servers - Ctrl-C in each terminal (API, Expo, and the worker if you started it). Expo may leave a Metro bundler port held; a second Ctrl-C or closing the terminal releases it.
  • Containers - from the repo root:
Terminal window
docker compose down # stops Postgres/Redis, KEEPS your data volume
docker compose down -v # ALSO deletes ./.docker-data - a clean slate

Use -v deliberately only: it wipes your local database, and your next start must re-run the schema push (step 4) and re-ingest data from scratch.

Common local failures at a glance

SymptomMost likely causeFix
Every authed request 401/500sJWT_SECRET blankSet any non-empty dev string in .env, restart API
/readyz returns 503 "db":"down"Postgres not healthy / bad DATABASE_URLdocker compose ps; wait for healthy; match DATABASE_URL
Schema push errors on citextOld DB volume without the extensiondocker compose down -v then re-push
Device app can’t reach the APIOrigin not in CORS_ALLOWED_ORIGINSAdd the LAN origin, restart the API
No fires/wind on the mapSchedulers haven’t cycled yetWait a minute; check /readyz ingest ages
Wind simulation “does nothing”REDIS_URL unset → queue darkExpected - opt in per step 7, or ignore

For anything not listed here, see Troubleshooting.