Skip to main content

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.env carries several historical APP_PORT blocks 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 — a Secure refresh cookie over plain HTTP is silently dropped by the browser and login hangs (see the memory of that diagnosis; production keeps it true).

  • 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 /data
    AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
    aws --endpoint-url http://localhost:9000 s3 mb s3://uhp-documents

    The bucket is not auto-created — S3DocumentStorage writes to uhp-documents and fails without it. (The localstack/localstack:latest image quits on start demanding a license token; MinIO needs nothing.) Uploads land scan_status = PENDING and stay there locally — no scanner is wired — which is fine for FILE form fields (only INFECTED is 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 connection and a 5–20 s pause while the pool replaces the corpse — long enough for the Next.js dev proxy to give up with ECONNRESET. 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

  1. http://localhost:3000/loginsuper_admin + the current password (rotates; it lives in .env.local as E2E_SUPER_ADMIN_PASSWORD. ⚠ One wrong guess is recoverable; repeated failures lock the account — if the stored password is stale, ask, don't retry).
  2. The scope walk: Select tenantSelect facility → Continue. The builder is facility-scoped, so the session must carry both claims (/session/scope rotates the token). The Global tenant owns no facilities — pick an operating tenant that does (the E2E demo authority's E2E Demo Clinic works on the dev database).
  3. 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 / ECONNRESET in the frontend log;
  • the login page stuck on "Opening dashboard", or /forms/builder stuck 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 (see refresh-token-reuse.md; the security behavior is correct, the transport was the problem);
  • HikariPool-1 - Failed to validate connection warnings 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.