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:
| Workspace | Key scripts | Purpose |
|---|---|---|
root (workspace) | typecheck, typecheck:libs, build | Cross-workspace typecheck; builds lib project-refs first |
@workspace/api-server | test, test:coverage, seed, build, dev | The Node/Express API + Vitest suite |
@workspace/firepath-ai | test, theme:check, wcag, web | The Expo app + client unit tests + a11y audits |
@workspace/docs-site | gen, check:drift, ci, deploy:pages | These docs + the drift/redaction/link gates |
@workspace/marketing-site | ci, deploy | www. static site + its redaction gate |
Workflow
- Branch off
mainfor every change.mainis protected; direct pushes are not the path (all git write operations are performed by the maintainer, never automatically). - 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.
- 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. - 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.
- Open the PR against
main. ThePRworkflow (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
# from Fire-Path-AI/pnpm typecheckThis 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:
# 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 + citextpnpm --filter @workspace/db run push-force # push Drizzle schemapnpm --filter @workspace/api-server run seed # load fixturespnpm --filter @workspace/api-server run test:coverage # run suite + coverage ratchetDATABASE_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
pnpm --filter @workspace/firepath-ai run test # pure-logic client suite (Vitest)pnpm --filter @workspace/firepath-ai run theme:check # typecheck + colour-token + WCAG audittheme: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 page | Script | Source of truth |
|---|---|---|
| Configuration | gen:config | the config/env definitions |
| Data model | gen:datamodel | the Drizzle schema |
| API reference | gen:api | the Zod/API spec |
| Repository map | gen:repotree | curated repo-tree allowlist |
Regenerate all four and commit whatever changes:
pnpm --filter @workspace/docs-site run gen # runs all four generatorspnpm --filter @workspace/docs-site run check:drift # asserts committed output == freshly generatedcheck: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:
| Job | What it runs | Why |
|---|---|---|
| Typecheck | pnpm typecheck | Whole-workspace type safety |
| Client unit tests | pnpm --filter @workspace/firepath-ai run test | Pure-logic client suite |
| Dependency + secret scan | pnpm audit --audit-level=high, gitleaks over the diff | Supply-chain + leaked-secret safety net |
| SPA theme + WCAG | typecheck:libs then theme:check | Colour tokens + contrast, zero failures |
| docs-site build + gates | docs-site ci: gen → drift → reference → build → redaction → links | Docs match code, publish clean |
| marketing-site build + gate | marketing-site ci: build → redaction | www. publishes clean |
| api-server vitest | PostGIS service → bootstrap SQL → push-force → seed → test:coverage | Integration tests + coverage ratchet |
Notes that save a re-run:
- The api-server job spins up its own
postgis/postgis:16-3.4service 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-runningpushlocally - reproduce it with the local commands, not by re-running CI. - CI runs with production secrets unset.
JWT_SECRETin the api-server job is a throwaway string andDATABASE_URLpoints 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
ciscript differs from itsbuildscript:ciaddscheck:driftup front. Locally, runpnpm --filter @workspace/docs-site run cito 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 builtdist/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:
| Profile | Channel | Distribution | Android artifact |
|---|---|---|---|
development | development | internal | APK (dev client) |
preview | preview | internal | APK |
production | production | store | app-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.
# from artifacts/firepath-ai/ (requires an authenticated eas-cli >= 14)eas build --profile preview --platform android # internal APK for on-device testingeas build --profile production --platform all # store submission artifactsHow 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
mainwhen docs paths change (or via manual dispatch). The deploy runs the fulldeploy:pagesscript - the samebuildgate chain, then upload - behind a required deploy token. Because docs arenoindexand 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_OKpublish 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:
- 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. - Subsequent code deploys go through the
Deploy api-server (prod)workflow, which isworkflow_dispatchonly and confirmation-gated: you must typedeployinto theconfirminput 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).