npm run qa:local runs the whole platform on your machine, offline, with synthetic seats and
data, so a person or an agent with a browser can sign in as any role and walk every app without
Auth0, a tenant, or production data.
From the repository root:
npm run qa:local # start, or report the stack that is already running npm run qa:local -- --status # print URLs, health and sign-in links npm run qa:local -- --fresh # stop, wipe and start again on freshly seeded data npm run qa:local -- --stop # stop every process of the stack
| Option | Default | Effect |
|---|---|---|
--port-base N | 4310 | Main on N, Spell review on N+1, Allocation risk on N+2, identity provider on N+9. Also read from QA_LOCAL_PORT_BASE |
--volume N | 60 | Number of synthetic #83 volume processes; 0 seeds none (0–5000) |
--skip-build | off | Skip scripts/build-packages.mjs when the workspace packages are already built |
The stack runs on the Node major that package.json pins (engines, Node 22). Started from another
major, qa:local looks for a Node 22 install (Homebrew node@22, nvm, Volta, fnm) and runs every
process on it; QA_LOCAL_NODE=/path/to/node chooses one explicitly. Node 26 cannot compile Main's
pages under next dev, because it loads applications/main/tailwind.config.ts as ESM.
What it starts, in order, all on 127.0.0.1 (ports for the default base):
| Process | Port | What it is |
|---|---|---|
| identity | 4319 | Loopback OAuth server with a seat picker (applications/main/src/local/identity-provider.ts) |
| main | 4310 | next dev for Main: backend API, generic frontend and operator console |
| worker | none | The step worker, one, on the same store and policy as Main |
| spell | 4311 | next dev for the Spell review frontend, proxying to Main |
| risk | 4312 | next dev for the Allocation risk frontend, proxying to Main |
Before the web hosts start it builds the packages, writes the local access policy, directory and
seats into .qa-local/run, starts the identity provider, runs the release step
(the template publication npm run release runs) and seeds synthetic processes. The first start takes
about 1.5 minutes, most of it next dev compiling. When everything is healthy it prints the seat
picker URL and a direct sign-in link for every seat.
A start that is not reusing a healthy stack always wipes .qa-local/run and .qa-local/logs
first. If any stage fails, every process already started is stopped and the error names the log
to read.
| Path | Contents |
|---|---|
.qa-local/run/ | File store, uploaded files, access.json, directory.json, seats.json, the provider's signing key |
.qa-local/logs/ | One <service>.log per stage and process: build-packages, prepare, identity, release, seed, main, worker, spell, risk |
.qa-local/screenshots/ | Acceptance screenshots, from the checklists and npm run test:acceptance |
.qa-local/state.json | Ports, URLs and process ids of the running stack |
.qa-local/ is gitignored.
The seat picker lists every seat with one button per app; ★ marks the seat's home app. A direct link signs one seat into one app through the normal sign-in round trip:
http://127.0.0.1:4319/ seat picker http://127.0.0.1:4319/sign-in?seat=<seat>&app=<main|spell|risk>[&returnUrl=/path]
For example, the EPL into Spell review:
http://127.0.0.1:4319/sign-in?seat=spell-pm&app=spell. returnUrl must be a path on the
target app. To switch seats, open another direct link: the chosen seat overrides the one the
provider remembers.
The link goes to the app's login route, which redirects to the provider's authorize endpoint, which returns a code to the app's callback; the backend exchanges the code at the provider's token endpoint and verifies the token against the provider's JWKS. These are the endpoints the backend calls on Auth0, at the same paths:
http://127.0.0.1:4319/authorize http://127.0.0.1:4319/oauth/token http://127.0.0.1:4319/.well-known/jwks.json http://127.0.0.1:4319/directory.json stands in for the Auth0 Management API
Every hop is the production code path; only the person is chosen locally.
| Seat | Person | Organization | Spell review | Allocation risk | Main | Home |
|---|---|---|---|---|---|---|
prime-member | Pat Prime | Grove | prime-member | prime | — | spell |
prime-member-spark | Sam Spark | Spark | prime-member | — | — | spell |
sky-core-user | Casey Core | Sky Core | sky-core-user | — | — | spell |
soter-user | Sol Soter | Soter Labs | soter-user | — | — | spell |
spell-pm | Erin EPL | Soter Labs | spell-pm | — | — | spell |
spell-risk-reviewer | Bea Labs | BA Labs | spell-risk-reviewer | — | — | spell |
spell-tech-coordinator | Tess Tech | Soter Labs | spell-tech-coordinator | — | — | spell |
govops-pm | Gil GovOps | Soter Labs | — | govops-pm | — | risk |
head-of-risk | Hal Risk | BA Labs | — | head-of-risk | — | risk |
sff-board | Fern Board | Sky Core | — | sff-board (read-only) | — | risk |
soter-admin | Ada Admin | Soter Labs | soter-user | govops-pm | operator, plus reader on each volume app | main |
soter-admin is also the platform operator: templates:read, database:read, database:write
and user:impersonate. Every seat's email is <seat>@local.example.test and its user id is
local|<seat>. Seats are defined in applications/main/src/local/seats.ts; role permissions come
from the local deployment access policy written there, not from the token.
Every process is started and advanced through the normal application service, each step by the
seat that owns it, so ownership, audit and permissions are what the running app would have
produced. Source: applications/main/src/local/seed.ts; the result is in .qa-local/logs/seed.log.
Spell review, all for the second offered vote date (the cadence rule's, so a day rolling over cannot drop it):
| Item | Filed by | Parked at | Acts next |
|---|---|---|---|
| Grove draft: raise the Grove ALM line | prime-member | s01_deal_registration (unsubmitted draft) | prime-member |
| Grove: onboard synthetic RWA vault | prime-member | s05_validate_spell_item_request | spell-pm |
| Grove: adjust synthetic PSM parameters | prime-member | s07a_pre_risk_review_write | spell-risk-reviewer |
| Spark: rebalance synthetic allocation | prime-member-spark | s06_triage_and_prioritize | spell-pm |
| Sky Core: rotate synthetic oracle | sky-core-user | s05_validate_spell_item_request | spell-pm |
| Grove: synthetic cBEAM rate change | prime-member | s08_prepare_strategic_review | spell-pm or spell-tech-coordinator |
| Spark: synthetic strategic decision | prime-member-spark | s09_strategic_decision | spell-pm |
| Grove: synthetic technical scope | prime-member | s12_independent_technical_review | spell-tech-coordinator |
Allocation risk, all filed by prime-member for Grove:
| Deal | Parked at |
|---|---|
| Grove synthetic facility A | s01_deal_registration |
| Grove synthetic facility B | s02_nda_check |
| Grove synthetic facility C | s02_nda_check |
Main (operations): one run, started by soter-admin, of every repository template whose first
step waits for a person (the NFAT, onboarding, agent and Solana bridge templates). Templates whose
first step is automated are not started.
Volume: the #83 list-performance seed sized for a laptop: 60 processes across three
applications, volume-a (30), volume-b (18) and volume-c (12), at /apps/volume-a and so on
in Main. --volume changes the total.
scripts/qa-local/loopback-only.cjs
preloaded. A socket connection to anything but 127.0.0.1, localhost, ::1 or a local
socket throws before a packet leaves the machine. This is a regression guard, not a sandbox for
hostile code. The package build stage runs without it.PATH, HOME, TMPDIR, TERM,
LANG, USER and SHELL from your shell. Every key defined in a .env* file (except
.env.example) at the root, applications/main, applications/spell-review or
applications/allocation-risk is set to the empty string, which Next and dotenv never override.STORAGE_DRIVER=file and MONGO_URL empty; data and uploads live under
.qa-local/run..qa-local/run/identity-key.json, mode 600). The backend verifies signature, issuer, audience
and organization through the normal path. The issuer is https://local.processos.invalid/: the
reserved .invalid domain can never be a real tenant.directory.json on the provider). Invitations are refused.The backend accepts local identity only when PROCESS_LOCAL_IDENTITY_URL is set and every
rule below holds (platform/backend/src/auth/local-identity.ts). Otherwise it throws
LocalIdentityRefused; it never falls back to Auth0.
PROCESS_LOCAL_IDENTITY_URL is a plain http loopback origin: host 127.0.0.1, localhost
or ::1, no credentials, no path, query or fragment.RAILWAY_* variable is present (RAILWAY_ENVIRONMENT, RAILWAY_PROJECT_ID and the
other Railway names are also checked by name).VERCEL, VERCEL_ENV, FLY_APP_NAME, K_SERVICE, RENDER, DYNO,
KUBERNETES_SERVICE_HOST, ECS_CONTAINER_METADATA_URI, ECS_CONTAINER_METADATA_URI_V4,
AWS_LAMBDA_FUNCTION_NAME, GAE_SERVICE or WEBSITE_SITE_NAME is set.AUTH0_DOMAIN ends in .invalid.AUTH0_MGMT_CLIENT_ID and AUTH0_MGMT_CLIENT_SECRET are unset or empty.APP_BASE_URL, NEXT_PUBLIC_APP_BASE_URL and AUTH0_JWKS_URL, when set, are loopback URLs.PROCESS_AUTH_MOUNTS and CORS_ALLOWED_ORIGINS, when set, is loopback.MONGO_URL, when set, is mongodb:// (not mongodb+srv://) with only loopback hosts.The check runs at server startup (Next instrumentation), at host composition (web and worker), on
every use (authorize, token exchange, key source, directory), and before the identity and seed
stages of applications/main/scripts/local-acceptance.ts.
One per app, each a numbered walk with the seat, the expected result, a screenshot name and what counts as a failure. An agent with a browser (Claude in Chrome or Playwright) follows them against a running stack.
Screenshots go to .qa-local/screenshots/<app>-<nn>-<name>.png.
npm run test:acceptance
Runs applications/main/tests/acceptance/*.spec.ts with the acceptance Playwright configuration
(playwright.acceptance.config.ts in applications/main) against a stack it first restarts with
npm run qa:local -- --fresh, because the journeys change the seed. With a stack already running,
node_modules/.bin/playwright test -c applications/main/playwright.acceptance.config.ts runs them
without the restart. Its screenshots are named acceptance-<app>-<nn>-<name>.png, next to the
checklists' <app>-<nn>-<name>.png. Screenshots land in .qa-local/screenshots/. It is not part
of npm run verify; see verification.
The previous local workflow was the feat/dev-local-auth branch (PRs #50 and #35): a
dev-login route taking ?role=…, run as two terminals (npm run dev and npm run job:step)
because npm run dev:all broke it. Its failure modes, and why they do not apply here:
| Old failure | Cause | Now |
|---|---|---|
401 at middleware under dev:all | The JWKS URL was set at runtime inside a route handler; the middleware never saw it | The provider starts before the web host and its URL is in the environment at startup |
| Dev tokens rejected after the first request | next dev cached the Auth0 JWKS URL from the first request | No Auth0 URL is ever configured; the key source is the provider from the first request |
| Real Auth0 token exchange failed locally | Socket Firewall's NODE_EXTRA_CA_CERTS | No Auth0 call and no TLS; the variable is not passed to the stack |
The branch is not needed: sign in with a seat link instead of a role query.
| Symptom | Cause | Fix |
|---|---|---|
port 4310 (main) is in use | Another server, or another worktree's stack, holds the port | npm run qa:local -- --port-base 4410; links and ports shift with it |
<service> did not become healthy or <service> exited | The named service failed | Read .qa-local/logs/<service>.log |
Local identity is refused: … | One of the guard rules above failed; the message names it | Run through npm run qa:local, which sets a permitted environment |
seed fails | A seeded step was refused | .qa-local/logs/seed.log names the item and step |
| Data is in a state you did not expect | Earlier walks changed it | npm run qa:local -- --fresh |
Sign-in loops or a session is rejected after --fresh | The signing key was regenerated | Open the seat's direct link again |
| Odd compile errors or missing pages after switching branch | Stale next dev output | --stop, delete .next in applications/main, applications/spell-review and applications/allocation-risk, start again |
| A run sits on an automated step | The worker died | .qa-local/logs/worker.log; --status shows it DOWN |
next dev: the first load of each page compiles it, which can take several
seconds per page.s02_nda_check on) are left for the walk.--fresh: the run directory, including the signing key, is wiped..next directory, so qa:local must not run at
the same time as npm run dev, npm run build or npm run verify in the same checkout. Use a
second worktree for either..env files are set to the empty string, not unset, so code that defaults with
?? sees an empty value.