Skip to content

Contributing & release

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

How to contribute a change so it passes review and CI on the first try, and how a change actually reaches production. The full set of automated gates and the reasoning behind the coverage bar live in Testing & quality; this page is the day-to-day workflow, the commands with their expected behaviour, and the release path.

The one thing to understand first

This is a pnpm workspace (Fire-Path-AI/), not a single app. The root package.json declares packageManager: pnpm@11.x and wires several workspaces: shared libs under lib/* (@workspace/db, @workspace/api-zod, @workspace/spread-model, …) and the deployable artifacts under artifacts/* (api-server, firepath-ai the Expo app, docs-site, marketing-site). Almost everything you run is a filtered pnpm command against one of those workspaces - you rarely cd into a package.

The four CI-visible package families and their entry scripts:

WorkspaceKey scriptsPurpose
root (workspace)typecheck, typecheck:libs, buildCross-workspace typecheck; builds lib project-refs first
@workspace/api-servertest, test:coverage, seed, build, devThe Node/Express API + Vitest suite
@workspace/firepath-aitest, theme:check, wcag, webThe Expo app + client unit tests + a11y audits
@workspace/docs-sitegen, check:drift, ci, deploy:pagesThese docs + the drift/redaction/link gates
@workspace/marketing-siteci, deploywww. static site + its redaction gate

Workflow

  1. Branch off main for every change. main is protected; direct pushes are not the path (all git write operations are performed by the maintainer, never automatically).
  2. Make the change, keeping it scoped and covered by tests. The bar is happy path, errors, boundaries, auth/RBAC, and concurrency where relevant - see Testing & quality for what “covered” means and why the coverage ratchet will reject a PR that dilutes it.
  3. Regenerate the drift-gated reference and commit it. Four pages under /reference/* are generated from the code, not hand-written. If your change touches routes, the Drizzle schema, or configuration, you must regenerate and commit the output, or CI’s drift gate fails the PR. See Regenerating the generated reference.
  4. Typecheck and test locally so the PR is green before you open it - the exact commands are in the next two sections, and the copy-paste happy path is in Verify.
  5. Open the PR against main. The PR workflow (below) runs seven jobs; all must be green.

Running the checks locally

These are the same commands CI runs, so a clean local run is the best predictor of a green PR.

Typecheck the whole workspace

Terminal window
# from Fire-Path-AI/
pnpm typecheck

This runs typecheck:libs first (tsc --build --force over the lib project references, which emits the declaration files the artifacts import), then type-resolves every artifact and the scripts package. No database and no secrets are needed - it is pure type resolution. If you skip typecheck:libs and get “cannot find module @workspace/db” errors, that is the tell: the lib .d.ts files were never built. Running pnpm typecheck (not just an artifact’s local tsc) fixes it because it builds the libs first.

api-server tests against a real PostGIS Postgres

The suite is integration-first: it runs against a genuine PostGIS database, not a mock, because the spatial queries are the product. Bring the DB up, prepare the schema, seed fixtures, then run Vitest - exactly the CI order:

Terminal window
# from Fire-Path-AI/ (Postgres+PostGIS must be reachable on DATABASE_URL)
psql "$DATABASE_URL" -f lib/db/migrations/0000_init_postgis.sql # enable postgis + citext
pnpm --filter @workspace/db run push-force # push Drizzle schema
pnpm --filter @workspace/api-server run seed # load fixtures
pnpm --filter @workspace/api-server run test:coverage # run suite + coverage ratchet

DATABASE_URL matches docker-compose so the local run mirrors CI - postgres://firepath:dev_only_firepath@localhost:5432/firepath. Local start/stop of Postgres and Redis, plus JWT_SECRET, is covered in Verify; that page is the friendly copy-paste, this one explains the ordering.

Client unit tests and the theme/a11y audit

Terminal window
pnpm --filter @workspace/firepath-ai run test # pure-logic client suite (Vitest)
pnpm --filter @workspace/firepath-ai run theme:check # typecheck + colour-token + WCAG audit

theme:check chains typecheck → theme:audit → wcag. The theme audit enforces the design-token rule (no raw hex in components - colours come from tokens so light/dark and dichromacy hold), and the WCAG pass checks contrast. Both must report zero failures; the SPA theme CI job runs the same theme:check.

Regenerating the generated reference

The /reference/* pages are produced by tsx generators in docs-site/scripts/ that read the source of truth directly, so the docs cannot silently drift from the code:

Generated pageScriptSource of truth
Configurationgen:configthe config/env definitions
Data modelgen:datamodelthe Drizzle schema
API referencegen:apithe Zod/API spec
Repository mapgen:repotreecurated repo-tree allowlist

Regenerate all four and commit whatever changes:

Terminal window
pnpm --filter @workspace/docs-site run gen # runs all four generators
pnpm --filter @workspace/docs-site run check:drift # asserts committed output == freshly generated

check:drift regenerates in memory and diffs against the committed files; a non-empty diff fails the PR. The recovery is always the same: run gen, commit the result, push. Do not hand-edit a /reference/* page - your edit is the next drift failure.

The CI gates that must pass

Opening or updating a PR runs the PR workflow. Every job must pass:

JobWhat it runsWhy
Typecheckpnpm typecheckWhole-workspace type safety
Client unit testspnpm --filter @workspace/firepath-ai run testPure-logic client suite
Dependency + secret scanpnpm audit --audit-level=high, gitleaks over the diffSupply-chain + leaked-secret safety net
SPA theme + WCAGtypecheck:libs then theme:checkColour tokens + contrast, zero failures
docs-site build + gatesdocs-site ci: gen → drift → reference → build → redaction → linksDocs match code, publish clean
marketing-site build + gatemarketing-site ci: build → redactionwww. publishes clean
api-server vitestPostGIS service → bootstrap SQL → push-forceseedtest:coverageIntegration tests + coverage ratchet

Notes that save a re-run:

  • The api-server job spins up its own postgis/postgis:16-3.4 service container with the same credentials as docker-compose, then runs the exact four-step sequence above. If it fails at “Bootstrap DB extensions” or “Push Drizzle schema”, the cause is almost always a schema change you made without re-running push locally - reproduce it with the local commands, not by re-running CI.
  • CI runs with production secrets unset. JWT_SECRET in the api-server job is a throwaway string and DATABASE_URL points at the disposable service container; every PR gets a fresh DB, so token replay is a non-issue. Any test that requires a real secret to pass is a bug - mock it, or defer it to a wake. This is a hard rule; see Security.
  • The docs ci script differs from its build script: ci adds check:drift up front. Locally, run pnpm --filter @workspace/docs-site run ci to reproduce the docs job exactly.

Commit & content hygiene

  • Keep commits focused and messages descriptive of the change. Do not reference tooling, assistants, or internal identities in commit messages, PRs, code comments, or any repo artefact.
  • Never commit secrets or personal/infrastructure identifiers. The gitleaks scan and the docs redaction gate (redaction-gate.mjs, run over the built dist/ of both static sites) are a safety net, not a licence to be careless. Secrets live in AWS Secrets Manager and gitignored config - never in the repo (see Security).
  • Non-secret defaults must stay non-personal role values; environment-specific values are supplied via env/secrets, not hard-coded literals. The generated configuration reference is the canonical list of what is a variable versus a default.

The mobile app build (EAS)

The Expo app (artifacts/firepath-ai) is built with EAS Build, configured in eas.json with three profiles:

ProfileChannelDistributionAndroid artifact
developmentdevelopmentinternalAPK (dev client)
previewpreviewinternalAPK
productionproductionstoreapp-bundle (autoIncrement)

All three point EXPO_PUBLIC_API_BASE_URL at https://api.firepath.software/api, so an installed build only has live data when the AWS backend is HOT - against a COLD backend the app shows its offline/degraded state, which is expected, not a build failure.

Runtime version policy is appVersion (app.json), and eas.json sets appVersionSource: "remote". Together these mean: an OTA update is only delivered to a build whose runtime version matches, and the runtime version is tied to the app’s version. The practical rule - bump app.json version for any change that alters native code or the JS↔native contract, so incompatible JS is never pushed OTA to an older binary. The production profile autoIncrements the build number for you; the user-facing version is yours to set.

Terminal window
# from artifacts/firepath-ai/ (requires an authenticated eas-cli >= 14)
eas build --profile preview --platform android # internal APK for on-device testing
eas build --profile production --platform all # store submission artifacts

How web + docs releases reach production

The three Cloudflare surfaces (app., docs., www.) are static and always-on; publishing them is a Cloudflare Pages/Workers deploy, not an AWS wake.

  • docs-site deploys on push to main when docs paths change (or via manual dispatch). The deploy runs the full deploy:pages script - the same build gate chain, then upload - behind a required deploy token. Because docs are noindex and Access-gated, they can ship while incomplete.
  • marketing-site is manual dispatch only - it never auto-deploys on merge. Marketing is public and indexed, so its release stays a deliberate human action. (The former MARKETING_PUBLISH_OK publish gate was removed 2026-08-03 once the copy review cleared; the conservative-copy rules still apply on every change, enforced by review.)

How a release reaches the AWS backend (production)

The API backend is hibernated by default (cost ~$0 idle), so shipping server code is a deliberate action during a wake, not a push-on-merge:

  1. A wake brings the stack up and deploys the first image; on boot the api task self-bootstraps its database (migrate + seed) and updates the api. DNS record.
  2. Subsequent code deploys go through the Deploy api-server (prod) workflow, which is workflow_dispatch only and confirmation-gated: you must type deploy into the confirm input or the job hard-fails at the guard step. It authenticates to AWS via OIDC (no long-lived keys), builds and pushes the image to ECR, renders a new ECS task definition, deploys and waits for the service to stabilise, then runs a post-deploy smoke test against the live endpoint. A failing smoke test fails the workflow.

The wake/hibernate mechanics, the DB self-bootstrap, auto-DNS, and the cost model live in Operations; the hosting topology is in Infrastructure.

Where to go next

  • Verify - the copy-paste local bring-up and health checks.
  • Testing & quality - the coverage bar, gate internals, and the “CI never gets real secrets” rule.
  • Operations - deploy pipeline, wake/hibernate, bootstrap.
  • Configuration reference - every environment variable (generated from the code).