Skip to content

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 use picks it up.
  • pnpm — the package manager for the backend and JS tooling.
  • Flutter via FVM — version pinned in .fvmrc. Use fvm 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):

fvm flutter pub run build_runner build --delete-conflicting-outputs

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:

flutter pub get          # updates pubspec.lock
git add pubspec.lock

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.yaml excludes 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, the Code complexity job).
  • Every Flutter package is a workspace member (resolution: workspace). A package with its own .dart_tool/package_config.json (left behind by running pub get inside it) is also a separate context; flutter pub get at 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.

cd apps/perci-platform-backend/web-professionals
pnpm install
pnpm run dev             # vite
pnpm run lint

Building these docs

The docs toolchain is managed by uv (pyproject.toml + uv.lock). uv installs Python and the dependencies on first run.

uv run mkdocs serve          # live preview at http://127.0.0.1:8000
uv run mkdocs build --strict # what CI runs; fails on broken links