Running the local stack (FORM-100)
The exact commands that bring up a working local platform — backend answering its health endpoint,
frontend serving, super_admin able to reach the form builder — recorded so nobody re-derives them.
Measured working 2026-08-21 on the development machine.
Backend — the single platform-app jar
cd backend # a worktree with .env present (application.yml imports optional:file:.env)
./mvnw -q -pl app -am install -DskipTests -Dcheckstyle.skip -Dpmd.skip -Dspotbugs.skip -Djacoco.skip
APP_PORT=8081 \
LOCAL_AUTH_REFRESH_COOKIE_SECURE=false \
SPRING_DATASOURCE_HIKARI_KEEPALIVE_TIME=60000 \
SPRING_DATASOURCE_HIKARI_MAX_LIFETIME=300000 \
SPRING_DATASOURCE_HIKARI_VALIDATION_TIMEOUT=2000 \
SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT=10000 \
S3_ENDPOINT_URL=http://localhost:9000 \
DOCUMENTS_ACCESS_KEY=minioadmin \
DOCUMENTS_SECRET_KEY=minioadmin \
java -jar app/target/platform-app-0.0.1-SNAPSHOT.jar
Why each override exists:
-
APP_PORT=8081—.envcarries several historicalAPP_PORTblocks and the last one wins (8085 at the time of writing). The frontend proxy expects 8081, and an OS environment variable outranks the imported file, so set it at launch rather than editing.env. -
LOCAL_AUTH_REFRESH_COOKIE_SECURE=false— aSecurerefresh cookie over plain HTTP is silently dropped by the browser and login hangs (see the memory of that diagnosis; production keeps ittrue). -
The three S3 variables — document uploads (FILE form fields, patient documents) need an S3-compatible store. Locally that is MinIO:
docker run -d --name uhp-minio -p 9000:9000 \-e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /dataAWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \aws --endpoint-url http://localhost:9000 s3 mb s3://uhp-documentsThe bucket is not auto-created —
S3DocumentStoragewrites touhp-documentsand fails without it. (Thelocalstack/localstack:latestimage quits on start demanding a license token; MinIO needs nothing.) Uploads landscan_status = PENDINGand stay there locally — no scanner is wired — which is fine for FILE form fields (onlyINFECTEDis refused) but means downloads refuse. -
The four Hikari settings — on the development machine, idle JDBC connections to the local PostgreSQL die (VPN/firewall on the box is the prime suspect; the server itself sets no timeouts). Without keepalives every stall shows up as
HikariPool-1 - Failed to validate connectionand a 5–20 s pause while the pool replaces the corpse — long enough for the Next.js dev proxy to give up withECONNRESET. Keepalive pings (60 s) stop the connections dying; the short validation/connection timeouts make any survivor cost milliseconds instead of a hang. Production topology (Patroni + HAProxy) has its own keepalive story and does not use these values.
First boot against a stale database is slow (Liquibase catch-up; ~6 min was measured after a
week of merges) and readiness genuinely lags "Started": the permission-catalog sync runs after
the startup banner, and /actuator/health reports OUT_OF_SERVICE until ApplicationReadyEvent.
Wait for {"status":"UP"}:
curl -s http://localhost:8081/actuator/health
# {"status":"UP","groups":["liveness","readiness"]}
⚠ A 404 on /actuator/health means you are on the wrong port (see the APP_PORT trap above) —
the endpoint is exposed and permitted anonymously; it never 404s on the running app.
Frontend — Next.js dev server
cd frontend # nvm Node 22/24
cp <an existing worktree>/.env.local .env.local # gitignored: proxy target + super_admin creds
# ensure: API_PROXY_TARGET="http://localhost:8081"
npm ci
npm run dev # serves http://localhost:3000
Reaching the form builder as super_admin
http://localhost:3000/login—super_admin+ the current password (rotates; it lives in.env.localasE2E_SUPER_ADMIN_PASSWORD. ⚠ One wrong guess is recoverable; repeated failures lock the account — if the stored password is stale, ask, don't retry).- The scope walk: Select tenant → Select facility → Continue. The builder is
facility-scoped, so the session must carry both claims (
/session/scoperotates the token). The Global tenant owns no facilities — pick an operating tenant that does (the E2E demo authority'sE2E Demo Clinicworks on the dev database). http://localhost:3000/forms/builder.
⚠ The failure mode that looks like five different bugs
Under memory pressure (this is a 16 GB machine; a VM, a second agent's Maven build, the JVM, the dev server and a browser exceed it) or with the Hikari overrides missing, one root cause fans out into everything below — none of which is a code defect:
Failed to proxy … socket hang up / ECONNRESETin the frontend log;- the login page stuck on "Opening dashboard", or
/forms/builderstuck on "Checking access"; - a successful
Scoped session…line in the backend log yet the UI never advances — the response was lost, the client retried its refresh with an already-rotated token, and refresh-token reuse detection killed the session family (seerefresh-token-reuse.md; the security behavior is correct, the transport was the problem); HikariPool-1 - Failed to validate connectionwarnings in the backend log.
The fix is environmental: free memory (wait for concurrent builds), keep the Hikari keepalive overrides, and log in again from the login page.