Running locally¶
Getting the apps and backend running on your machine. This summarises the commands in
AGENTS.md;
that file is the source of truth for tooling.
Prerequisites¶
- Node 22.x (see
.nvmrc/.node-version).nvm usepicks it up. - pnpm — the package manager for the backend and JS tooling.
- Flutter via FVM — version pinned in
.fvmrc. Usefvm flutter .... - Melos — manages the Flutter monorepo workspace.
Flutter apps¶
melos bootstrap # install deps across the workspace
melos analyze # static analysis
melos test # run all tests
melos format # format
melos build_runner # codegen (Riverpod, freezed, etc.)
# single test
flutter test test/specific_test.dart
Run codegen after touching anything annotated (@riverpod, freezed, json):
Always run melos analyze && melos test before committing.
Changing a Flutter dependency¶
pubspec.lock at the repo root is the whole workspace's lockfile — the four members carry
resolution: workspace, so pub does one resolution for all of them and writes one lock. CI and
every deploy resolve with --enforce-lockfile, meaning they install exactly what is locked and
fail rather than pick something newer.
So any change to a dependency has to bring the lockfile with it:
Each package declares its own dependencies, and that is the only place a version lives. Pub resolves the whole workspace at once and picks one version per package, intersecting whatever the members declare, so two apps asking for compatible ranges of the same package still get the same version.
Enforcement exists because pub has no minimum-release-age setting (unlike .npmrc, which holds npm
packages for 24h): without the lock, a build would install whatever satisfies the caret ranges on
the day it runs, including a package published minutes ago. It also means two builds of the same
commit ship the same versions.
Renovate does this for you on its own PRs — it runs flutter pub get after applying an update and
commits the lockfile alongside it. The exception is a Flutter SDK bump, where the lockfile still has
to be refreshed by hand; that PR's body says so. See
Dependency updates.
Hot reload against a PR's preview backend¶
The deployed web previews are static builds, so they cannot hot reload — but every preview
backend has a deterministic host (backend-pr-<N>.perci.dev), so you can run the app locally
with real hot reload (r) / hot restart (R) against the same backend the preview uses:
scripts/frontend/run-against-preview.sh members 2086
scripts/frontend/run-against-preview.sh clinicians 2086
The script swaps in the staging config the preview pipeline uses, points the BFF URLs at the
PR's backend, starts flutter run -d chrome, and restores the swapped files from git on exit.
Run melos bootstrap and melos build_runner first. For a PR that did not change the backend,
pass any PR number and ignore the host — or just point at staging by running the app the normal
way.
The deployed previews get the next best thing automatically: each open preview tab polls the deployed build id and reloads itself (clearing the service worker) when a newer push lands, so reviewers never refresh by hand.
Golden tests¶
Goldens are excluded from melos test and run in a pinned Linux container, because golden output
is host-specific: glyphs rasterise through CoreText on macOS and FreeType on the CI runner, so the
same unchanged widget differs by a few percent of pixels between the two. The container makes any
machine — Mac, Linux or Windows — reproduce the CI rendering exactly.
melos run goldens # check against the committed baselines
melos run goldens_update # regenerate the baselines
# narrow to one package while iterating
PACKAGES=apps/perci-platform-clinicians melos run goldens
Needs Docker running. Extra arguments pass through to flutter test. Run melos build_runner
first in a fresh worktree, or the container will fail on stale generated code rather than on pixel
diffs.
Do not run flutter test --tags golden --update-goldens directly: on macOS it bakes your host's
text rendering into every baseline and CI then disagrees with all of them. The
Flutter - Update Goldens GitHub workflow does the same job on a CI runner if you would rather not
run Docker locally.
Analyzer performance¶
The pub workspace makes the repository root the analysis context for every Flutter package, so the IDE's Dart analysis server walks and watches everything below it. Three things keep that fast; undoing any of them brings back 10-15 s highlighting latency and a 4 GB analysis server:
- The root
analysis_options.yamlexcludes the non-Dart trees (build/,node_modules/, the backend, docs, infrastructure). Add new non-Dart top-level directories there. - No package lists
analyzer: plugins:. A legacy plugin (dart_code_linter,custom_lint) re-analyses its whole package inside the analysis server with no persistent cache, and a package whose plugin list differs from the root's becomes a separate analysis context. CI runs those rules instead (melos run lint_perci, theCode complexityjob). - Every Flutter package is a workspace member (
resolution: workspace). A package with its own.dart_tool/package_config.json(left behind by runningpub getinside it) is also a separate context;flutter pub getat the root deletes such stray files.
After changing any of these, restart the analysis server (Android Studio: the "Restart Dart Analysis Server" button in the Dart Analysis tool window; VS Code: "Dart: Restart Analysis Server").
Backend functions¶
Run from the package directory — these scripts are not exposed at the repo root.
cd apps/perci-platform-backend/functions
pnpm install
pnpm run dev # Firebase emulators (functions, firestore)
pnpm run serve # emulators incl. auth + pubsub
pnpm run test # vitest
pnpm run lint # eslint
pnpm run lint:fix
Run pnpm run lint && pnpm run test before committing.
Worktrees need Node 22 explicitly
In a fresh git worktree the default shell may be on Node 18, which breaks vitest. Run
nvm use 22 and pnpm install first. pnpm run build runs clean && generate && tsc -b,
and generate needs secrets; to just type-check, run tsc -b directly.
Web professionals¶
Also its own package; run from its directory.
Building these docs¶
The docs toolchain is managed by uv (pyproject.toml +
uv.lock). uv installs Python and the dependencies on first run.