Life ConnectLife Connect
Wiki index
Architecture
Services
Concepts
Runbooks
Infra
Swagger Docs
GitHub
Wiki index
Architecture
Services
Concepts
Runbooks
Infra
Swagger Docs
GitHub
  • Runbooks

    • Run the local stack
    • Deploy a PR preview
    • Provision a new environment
Last updated 2026-05-04

Run the local stack

AI-generated content

This document was generated by an AI assistant. Verify accuracy before relying on the details.

infra/docker/compose.yaml runs the supporting infrastructure (MongoDB replica set, Keycloak, LocalStack, Mailpit) AND the 9 Spring Boot services. Everything is built from monorepo source — only Docker is required, no AWS credentials, no host-side Java/Maven, no external Maven repositories beyond Maven Central + Spring Milestones.

First-time setup (zero → working SPA)

The shortest path that gets you to a logged-in dev/dev on http://localhost:4200/ showing the home dashboard, in French, against real seeded data.

Prerequisites

  • Docker Desktop (or any Docker engine + compose v2). Allocate ≥ 8 GB RAM to the engine — the full stack peaks around 6 GB.
  • An Atlas snapshot tarball of the dev cluster — ask the team for a recent restore-<id>.tar.gz (the file the "Download Snapshot" button produces in the Atlas UI). Drop it in infra/seeds/ — that's where make seed looks first. The directory itself is git-tracked (via .gitkeep) but every *.tar.gz inside is gitignored, so a stray git add won't commit it. See Seeding the local Mongo from an Atlas dump for the full lookup order.
  • Read access on the i18n spreadsheet (https://docs.google.com/spreadsheets/d/1pSlwsb6XEZ_uzz-KtNgffFrzEgLR6WcToLvO-_IeVsc). It's currently public, no Google credentials needed.

One-time setup

cd infra

# 1. Create your local .env file from the template and add the i18n vars.
cp .env.example .env.local
chmod 600 .env.local

Open .env.local and uncomment / fill in just the translation block at the bottom (the rest is needed only if you want to point at real AWS dev infra — LocalStack covers everything for local dev):

SPREADSHEET_URL=https://docs.google.com/spreadsheets/d/1pSlwsb6XEZ_uzz-KtNgffFrzEgLR6WcToLvO-_IeVsc/export?format=tsv
CATALOG_GID=2016792665
COMMON_GID=1740544717
DOCUMENTS_GID=1844992713
PERSONS_GID=1558004497
NOTES_GID=1409729206
LISTS_GID=378239050
ACCOUNTING_GID=1607045627
CONTRACTS_GID=1161123401
PARTS_GID=59981804
SEARCH_GID=1022530540
NACE_GID=14446475
TICKET_GID=452972274
JEDI_GID=536707157
ADMIN_GID=1828090766

(Without these, make build-base falls back to empty translation stubs and the SPA shows raw keys instead of French/English text — the rest of the stack still works.)

Boot, seed, login

cd infra

# 2. Build all JARs (10–15 min first time, ~30s on rebuild).
#    Fetches translations from the Google Sheet using the env vars above.
make build-base

# 3. Boot infrastructure + every Spring service. Builds the per-service
#    images on first run (~1 min), caches afterwards.
make up-services

# 4. Seed the Mongo databases from the Atlas snapshot. Picks up the
#    most recent infra/seeds/restore-*.tar.gz automatically (with
#    ~/Downloads/ as legacy fallback); pass an explicit path with
#    `DUMP=/some/where/restore-XXX.tar.gz` if needed.
make seed

# 5. Open the SPA. Login: dev / dev.
open http://localhost:4200/

You should land on the home dashboard with menu items in French ("Propriétaires", "Locataires", "Mandats", "Sinistres", …). Switch to English from the language flag in the topbar — localStorage.language just flips between "fr" and "en" and reloads.

Day-to-day workflow

You changedWhat to run
Backend Java codemake build-base && docker compose -f docker/compose.yaml --profile services up -d --build --no-deps adb-<svc>
adb-ui Angular codenothing — wrangler dev watches dist/ and auto-reloads
compose.yaml env or wiringdocker compose -f docker/compose.yaml --profile services up -d --no-deps adb-<svc>
Mongo contentmake seed-clear && make seed DUMP=...
Keycloak realm-exportdocker volume rm adb-local_keycloak-data && make up-services (Keycloak only re-imports on a clean H2 DB)
make down-all                     # stop everything (containers stay around, volumes preserved)
make logs-all SERVICE=adb-views   # follow logs for one service
make ps                           # container health

Starting over from scratch

make reset                        # destructive: prompts for "yes"
                                  # nukes containers + volumes + locally-built
                                  # images, but leaves .env.local and
                                  # infra/seeds/*.tar.gz alone
make reset YES=1                  # skip the prompt (CI / scripted use)

make fresh                        # one-shot: reset + build-base + up-services + seed
make fresh YES=1                  # skip the prompt

Use reset when something has gone weird (corrupted mongo replica set, stuck volumes, mismatched JARs) and you want a guaranteed-clean rebuild. Use fresh to chain that with the full setup recipe.

Two-tier model

TargetWhat runsFirst-run time
make upmongo, keycloak, localstack, mailpit~30 s
make build-baseMaven build of every JAR (shared adb-local-build image)~10–15 min (first time), then cached
make up-servicesruns build-base then all Spring servicesfirst time = build-base + ~1 min, subsequent = ~30 s

Make-target reference

cd infra

# Just the supporting infra (mongo, keycloak, localstack, mailpit):
make up

# Or full stack (infra + 9 Spring services):
make up-services
make ps                           # see container health
make logs                         # follow infra logs
make logs-all                     # follow infra + service logs (pass SERVICE=adb-x for one)
make down-all                     # stop everything

# Foreground variants — everything streams to your terminal, Ctrl+C tears it down:
make up-fg
make up-services-fg

# Mongo seeding (see "Seeding the local Mongo from an Atlas dump" below):
make seed                         # auto-find infra/seeds/restore-*.tar.gz
make seed DUMP=/path/to/restore-XXX.tar.gz
make seed-clear                   # drop every adb-* db (keeps the volume)

# Nuclear option (see "Starting over from scratch" above):
make reset                        # tear down + remove volumes + remove images
make fresh                        # reset + build-base + up-services + seed

How services reach LocalStack

Every Spring service builds its SqsAsyncClient / SnsClient / S3Client via MessagingConfig / AwsS3Config. These read AwsEndpointOverride.fromEnv() — which checks AWS_ENDPOINT_URL and applies it as .endpointOverride(...) on the builder. The compose file sets AWS_ENDPOINT_URL=http://localstack:4566, so locally everything routes there. In production the env var is unset and the SDK targets real AWS as usual. (This is the equivalent of what global AWS_ENDPOINT_URL support gives you in AWS SDK 2.21+; the services still pin SDK 2.17, hence the helper.)

What's running

ContainerHost portNotes
mongo27018rs0 single-node replica set; container internal port stays 27017
keycloak8080Realm ADB pre-imported, dev user dev/dev (email aligned with the seeded Pierre Sacco — see Seeding the local Mongo)
localstack4566SQS/SNS/S3/Secrets Manager mock
mailpit8025 (UI), 1025 (SMTP)Catches outbound mail
adb-persons8081/persons/actuator/health
adb-utilities8082
adb-files8088host port 8088 → container 8083 (avoids local port clash)
adb-contracts8084
adb-parts8085
adb-aggregates8086
adb-accounting8089does not start locally without a valid Google Cloud service-account JSON in GOOGLE_SHEETS_CLIENT_CREDENTIALS. Skip in local dev unless you need accounting.
adb-reports8090actuator endpoint requires auth (returns 401, but server is up)
adb-views8092actuator endpoint requires auth (returns 401, but server is up)
adb-ui4200served by wrangler dev inside a node:20-bookworm container. First start runs npm install (~3–5 min) then ng build --watch; wrangler picks up changes on rebuild. Worker config in adb-ui/wrangler.jsonc

Verifying it works

# every Spring service (except adb-accounting + adb-ui — see caveats above)
for spec in 8081/persons 8082/utilities 8088/files 8084/contracts \
            8085/parts 8086/aggregates 8090/reports 8092/views; do
  port="${spec%/*}"; name="${spec#*/}"
  curl -fsS "http://localhost:$port/$name/actuator/health" || echo "  $port: not yet"
done

# Keycloak realm reachable
curl -fsS http://localhost:8080/realms/adb >/dev/null && echo "keycloak OK"

# LocalStack queues exist (24 queues + 24 DLQs = 48)
aws --endpoint-url http://localhost:4566 sqs list-queues \
  --queue-name-prefix local- | jq '.QueueUrls | length'

How the build works

The Java build happens in two layers:

  1. make build-base runs mvn -DskipTests install over the entire monorepo via the repo-root pom.xml aggregator. The output is a Docker image tagged adb-local-build:latest containing every spring-boot JAR.
  2. Each service's Dockerfile is a 3-line file: FROM eclipse-temurin:21-jre-alpine, COPY --from=adb-local-build:latest /workspace/<svc>/.../target/*.jar /app/app.jar, ENTRYPOINT.

This means:

  • The Maven dependency download happens once, not per service.
  • A code change anywhere triggers a rebuild of adb-local-build (Maven layer cache helps but isn't perfect).
  • For very fast iteration on a single service you can mvn package -pl :<svc> -am on the host (if you have Java + Maven) and rebuild only that container with docker compose build <svc>.

The aws-maven extension in each service's .mvn/extensions.xml is preserved (CI still uses it to publish to the prod S3 Maven repo) but .mvn/settings.xml no longer references the S3 URLs, so it's a no-op for local builds.

Common issues

  • Mongo binds to host port 27018, not 27017. macOS Docker Desktop's vmnetd driver sometimes holds 27017 even when lsof shows nothing.
  • adb-files binds to host port 8088, not 8083. Same reason.
  • First run of make build-base takes 10–15 minutes while Maven resolves all transitive deps (Spring Boot, Keycloak, AWS SDK, …). Subsequent runs are fast thanks to the BuildKit cache mount on /root/.m2/repository.
  • adb-accounting fails to start with "Error reading credentials from stream, 'type' field not specified" — the service requires a valid Google Cloud service-account JSON to connect to Google Sheets at startup. Either provide one via GOOGLE_SHEETS_CLIENT_CREDENTIALS or skip the service for local dev.
  • adb-ui first start is slow — runs npm install (~3–5 min) inside the container before ng build --watch kicks off, then wrangler dev boots. Subsequent restarts skip the install thanks to the named adb-ui-node-modules and adb-ui-angular-cache volumes. The legacy nginx-based Dockerfile (adb-ui/Dockerfile) is not used by the local stack any more.
  • adb-ui post-login lands on /denied — the Angular app's AuthGuard calls GET /persons/me immediately after login. If the local Mongo is empty, the call returns 204 and the SPA routes to /denied. Seed the local Mongo from a real Atlas dump first — see below.
  • /persons/me returns 403 "No JWT token found in request headers" — wrangler dev (workerd) silently strips the Authorization header from outbound subrequests when the originating browser request was credentialed (Cookie + Sec-Fetch-*). The worker forces credentials: "omit" on the proxied fetch in adb-ui/worker/index.js to keep the bearer untouched. If you re-introduce browser-style request semantics in that proxy, every backend call will start failing with this error.

Seeding the local Mongo from an Atlas dump

The Spring services start with empty databases. The SPA's home page needs at least the dev user's Person and matching Organization to render — without them GET /persons/me returns 204 and AuthGuard routes to /denied. Rather than fabricate fixtures, the local stack imports a real MongoDB Atlas snapshot.

Where to drop the tarball

Recommended: infra/seeds/ — git-tracked directory (via .gitkeep) with all *.tar.gz files gitignored. That's where make seed looks first, so the common workflow is just:

cd infra
cp /wherever/you/got/it/restore-<snapshot-id>.tar.gz seeds/
make seed                                          # auto-picks the most recent

make seed lookup order (the first hit wins):

  1. The DUMP= argument — e.g. make seed DUMP=/explicit/path.tar.gz.
  2. The $ATLAS_DUMP_PATH env var.
  3. The newest restore-*.tar.gz in infra/seeds/ (gitignored).
  4. The newest restore-*.tar.gz in ~/Downloads/ (legacy fallback, convenient for engineers used to the old workflow).

Why infra/seeds/? It keeps the tarball next to the rest of the local-stack artefacts (so it survives make reset, doesn't get lost in your Downloads folder), and the directory's gitignore protects you from ever committing a dump file by accident — only .gitkeep is tracked, every *.tar.gz is ignored.

# Verify the tarball is in place and gitignored:
ls -lh infra/seeds/                         # should show your restore-*.tar.gz
git check-ignore -v infra/seeds/restore-*.tar.gz   # confirms it's ignored

What the script (infra/scripts/seed-from-atlas-dump.sh) does:

  1. Extracts the WiredTiger files from the tarball.
  2. Boots a temporary mongo:7 against them with the embedded replica-set name.
  3. Force-reconfigures it as a single-node primary (Atlas snapshots ship with a stale replica-set config that won't elect a primary on its own).
  4. mongodumps every adb-* database, then mongorestore --drops into the stack's mongo container.
  5. Tears the temp container down.

The seeded dev identity lands on Pierre Sacco — pierre.sacco@life-connect.fr, organization 5f7325ec5d7c2205ca4688df, with agent / agent-admin / internal / application-admin roles. The Keycloak realm-export pins the dev user's email to that address so personRepository.findOneByUserEmail(...) resolves.

To swap to a different seeded user, change email in the users block of infra/docker/keycloak/realm-export.json, recreate the keycloak container (docker volume rm adb-local_keycloak-data then docker compose up -d keycloak), and clear the SPA's session storage.

Wipe + re-seed if you need a clean slate:

make seed-clear                       # drops every adb-* database
make seed DUMP=/path/to/another.tar.gz

Why the worker injects an organization-id header

The backend's AdbJwtGrantedAuthoritiesConverter filters the JWT's accesses claim by the organization-id request header — without it the user has zero scopes and every call 403s. The SPA only sets that header once it has loaded the user's organizations, but the very first GET /persons/me runs before that. The worker provides a fallback via the DEFAULT_ORGANIZATION_ID var in adb-ui/wrangler.jsonc (matching the seeded Pierre's org). Production deploys never hit the proxy code path, so production behaviour is unchanged.

Why every Spring service has extra_hosts: ["localhost:host-gateway"]

The browser logs in via http://localhost:8080/realms/ADB/..., which Keycloak embeds in the JWT's iss claim. The backend validates that JWT against SECURITY_ISSUER_URI, which has to resolve to the same string from inside the container — hence the host-gateway entry. If you change SECURITY_ISSUER_URI to e.g. http://keycloak:8080/..., every API call fails with JwtException: The iss claim is not valid.

Translation bundles (i18n)

adb-utilities exposes /utilities/i18n/<NAME>?language=<lang> for the SPA's MultiTranslateHttpLoader (defined in adb-ui/src/app/content/layout/layout.module.ts). The handler calls ResourceBundle.getBundle(<NAME>, locale, classloader), so each bundle (COMMON, CONTRACTS, PERSONS, ACCOUNTING, PARTS, NOTES, LISTS, DOCUMENTS, SEARCH, TICKET, CATALOG, NACE, JEDI, ADMIN) needs <NAME>_en.properties and <NAME>_fr.properties on the JAR's classpath. The repo only ships JEDI_*.properties — the others come from a private Google Sheet.

Build pipeline

make build-base invokes adb-utilities/pipeline_scripts/prepare-translations.sh inside the build image, before mvn install. The wrapper either:

  1. Fetches real translations from the prod sheet — when SPREADSHEET_URL and the 14 *_GID env vars are set in .env.local, the wrapper calls create_translations.sh, which downloads each tab as TSV and emits <NAME>_{en,fr}.properties into target/i18n/. The pom registers that directory as a resource, so the bundles end up on the classpath.
  2. Stubs — when those env vars are missing (the default for any engineer who doesn't have spreadsheet read access), it writes empty <NAME>_{en,fr}.properties files. /utilities/i18n/<NAME> then returns {} (200) instead of 500. The SPA shows raw translation keys (COMMON.SAVE, CONTRACTS.MANDATE.CREATE, …), which is the current local-dev experience — but no error storm in the console.

Enabling real translations

Get read access to the sheet (https://docs.google.com/spreadsheets/d/12gBJ99fHXDJjY84lx1dprXbj_bAzC5tK), copy the gid for each tab into .env.local (see the env var names in infra/.env.example under "adb-utilities translation bundles"), then re-run make build-base.

The Makefile sources .env.local and forwards the variables as Docker build-args; the Dockerfile re-injects them as env vars for the wrapper RUN step.

Per-environment secrets and env files

The compose stack runs against LocalStack by default — no real credentials needed. To run against the real dev / staging / prod AWS instead (or to seed AWS Secrets Manager from the same values), use the infra/.env.* files:

FilePurposeTracked in git?
infra/.env.exampleSchema reference (every key, no values)✅ yes
infra/.env.localYour local-dev values (against real dev infra if you want)❌ gitignored
infra/.env.stagingStaging values❌ gitignored
infra/.env.prodProduction values❌ gitignored
# Use a specific env file with docker compose
cd infra
docker compose --env-file .env.local -f docker/compose.yaml --profile services up

# Seed AWS Secrets Manager for an env (uses the file you point at)
./scripts/seed-secrets.sh staging .env.staging

The env files contain real credentials. They are chmod 600 and gitignored, but rotate values immediately if any file ever lands in chat / a PR / a screenshot.

Adding a new queue / event

The single source of truth is infra/src/catalog/services.js. After updating the catalog, refresh the queue list in infra/docker/localstack-init/bootstrap.sh and the matching env vars in infra/docker/compose.yaml (the cloud Spring profile reads each queue URL by name).

See also

  • Pulumi project overview — what the local stack mirrors.
  • EventBridge fanout — how the queues are wired in production.
Edit this page
Last Updated:
Contributors: Yevhenii Khudolii
Next
Deploy a PR preview