Real-time medical emergency routing and triage platform. High-concurrency patient/ambulance telemetry ingestion over Kafka, PostGIS-backed hospital routing (weighted Dijkstra: distance + ER wait time + traffic delay + patient severity), automatic triage classification, and multi-tenant isolation enforced by a verified JWT claim.
pulse-alert-common: shared DTOs, enums, and exception types.pulse-alert-core: the Spring Boot service: ingestion, triage, routing, dispatch, WebSocket alerts, REST API, security.
- JDK 21+
- Maven 3.9+
- Docker (for
docker-compose.ymland for the Testcontainers-based integration test) openssl(only needed if you want to mint your own dev JWTs : see below)
docker compose up -d
mvn clean install
mvn -pl pulse-alert-core spring-boot:runThe app starts on :8080. spring.flyway.enabled=true runs V1__init_schema.sql and
V2__postgis_extensions.sql against the pulsealert database automatically on boot.
Every endpoint except /actuator/health, /actuator/info, and the /ws/** WebSocket upgrade
requires a valid Authorization: Bearer <jwt> header. Tenant identity comes from the JWT's
tenant_id claim : that's the source of truth, not a request header. If you also send an
X-Tenant-ID header and it disagrees with the token's tenant_id claim, the request is rejected
outright rather than silently trusting one or the other.
For local development the app verifies tokens against a committed dev-only RSA keypair : no external identity provider needed to run this locally:
- Public key (used by the app to verify tokens):
pulse-alert-core/src/main/resources/certs/dev-jwt-public-key.pem - Private key (used only to mint test tokens):
scripts/dev-auth/dev-jwt-private-key.pem
This keypair exists purely so the project boots and is testable out of the box. Never reuse it
outside local development : anyone who clones this repo has the private key. For anything
beyond your own machine, generate a fresh keypair (or point at a real IdP) and override
JWT_PUBLIC_KEY_LOCATION.
Mint a test token:
bash scripts/dev-auth/generate-dev-jwt.sh tenant-a dev-user 3600(Args: tenant id, subject, TTL in seconds : all optional, shown are the defaults.)
The script writes to disk with permissions that don't always survive packaging/transfer : if
./generate-dev-jwt.sh gives you "Permission denied", just run it as bash generate-dev-jwt.sh
as shown above.
TOKEN=$(bash scripts/dev-auth/generate-dev-jwt.sh tenant-a dev-user 3600)
curl -X POST http://localhost:8080/api/v1/ingest/telemetry \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"ambulanceId":"AMB-1","latitude":40.7484,"longitude":-73.9857,"speedKph":45.0,"headingDegrees":90.0,"accuracyMeters":5.0,"recordedAt":"2026-07-31T12:00:00Z"}'
curl -X GET http://localhost:8080/api/v1/dispatch/active -H "Authorization: Bearer $TOKEN"WebSocket alerts: since browsers can't set custom headers on a WebSocket handshake, the token goes in a query param instead, and is cryptographically verified server-side before the connection is accepted:
ws://localhost:8080/ws/alerts?token=<jwt>
mvn testDijkstraRoutingEngineTestis a plain JUnit 5 unit test, no Docker required.TenantIsolationTestspins up a realpostgis/postgis:16-3.4container via Testcontainers : Docker must be running, and the first run will pull the image.
Everything below is a real issue hit while getting this running on a fresh Windows machine, kept here so the next person doesn't have to rediscover each one from scratch. Search for the error text you're seeing.
Docker Desktop isn't installed, or was installed in a terminal window you opened before installing it (PATH changes don't apply retroactively). Install from docker.com, then open a brand new terminal.
Docker Desktop's CLI is installed but the actual engine isn't running. Launch the Docker Desktop app itself from the Start menu (installing it isn't enough : it has to be running, check the system tray for the whale icon) and wait for it to say "Engine running" before retrying.
Your CPU's virtualization (Intel VT-x / AMD-V) isn't enabled, and/or the required Windows features are off. Fix, in order:
- Admin PowerShell:
(the Hyper-V line fails harmlessly on Windows Home : skip it there)
dism.exe /online /enable-feature /featurename:Microsoft-Hyper-V /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart - Reboot.
- Check Task Manager β Performance β CPU β "Virtualization". If it still says Disabled, it has
to be enabled in BIOS/UEFI setup directly (usually
Del/F2/F10at boot, then find "Intel VT-x" / "SVM Mode" under Advanced/Security and enable it) : no Windows-side command can flip this if the motherboard has it locked off.
Maven isn't a JDK component : it's a separate install. Download the binary zip from
maven.apache.org/download.cgi, extract it to somewhere
in your user folder (not C:\Program Files\... : the built-in zip extractor can fail there
without admin rights), then add the bin folder containing mvn.cmd to your PATH via
"Edit the system environment variables". Open a new terminal afterward.
You ran mvn.cmd with no command after it. Maven needs a goal, e.g. mvn clean install or
mvn test, not just mvn by itself.
Testcontainers: Could not find a valid Docker environment / BadRequestException (Status 400: {"ID":"",...
This is a known compatibility gap between older Testcontainers and modern Docker Desktop
(Docker Engine 29+ requires a newer Docker API version than Testcontainers 1.19.x, the version
Spring Boot 3.3.4's BOM pins by default, knows how to negotiate). This project's root pom.xml
already overrides this via:
<testcontainers.version>1.21.4</testcontainers.version>If you still hit this on a newer Docker Desktop release than what this was tested against, bump
that property to whatever the current Testcontainers release is
(mvnrepository.com/artifact/org.testcontainers/testcontainers)
and re-run mvn clean install.
Two different causes produce this identical error : check both:
- Interrupted first-time volume init. The Postgres image only runs its user/db setup script
the very first time its data volume is created; if that got interrupted (e.g. Docker Desktop
restarted mid-startup), the volume is left half-initialized and never actually creates the
pulsealertrole, forever, on every subsequent start. Fix : wipe and let it redo setup from scratch (safe if the app never successfully started against it, since nothing was written):docker compose down -v docker compose up -d - Port 5432 conflict with a native Postgres install. If something else on your machine is
already listening on 5432 (a leftover native Postgres Windows service is common : some
installers bundle one silently), your app connects to that instead of the Docker container,
and that instance obviously has no
pulsealertrole. Check for a conflict:If you see a second listener that isn't Docker (netstat -ano | findstr :5432 tasklist /FI "PID eq <the PID netstat shows>"com.docker.backend.exe) or its WSL relay (wslrelay.exe) : commonly a Windows service namedpostgresql-x64-...: stop it viaservices.msc(right-click β Stop, and set Startup type to Manual so it doesn't reclaim the port on next boot).
Something else on your machine already owns 8080 (very common : lots of dev tools default to it). Easiest fix, run on a different port instead of hunting down what's using it:
mvn -pl pulse-alert-core spring-boot:run -Dspring-boot.run.arguments=--server.port=8081
Adjust every :8080 in the curl examples above to whatever port you picked.
If you don't have an actual WSL Linux distro installed (only the WSL2 platform Docker Desktop
uses internally), Windows' own bash.exe stub in System32 intercepts the bash command and
has nothing to run, producing something like
execvpe(/bin/bash) failed: No such file or directory. Use Git Bash instead if you have Git
for Windows installed: right-click the project folder in File Explorer β "Open Git Bash here",
then run the script from that terminal.
- Tenant isolation is row-level, not datasource-level.
TenantRoutingDataSource(multitenancypackage) exists as an extension point for schema/datasource-per-tenant routing but isn't wired up. Every tenant currently shares oneDataSourceand schema, isolated by atenant_idcolumn plus a verified-JWT check on every request (TenantFilter). - Triage classification is automatic. Incoming
PatientVitalsDtoevents are classified byTriageService(a simple, deterministic vitals-threshold model : not clinically validated, don't treat it as a real triage protocol) and persisted onto the matchingPatientrow, keyed bytenant_id+external_reference_id(the device/EHR-suppliedpatientIdstring). If no matching patient exists yet, one is auto-provisioned inPENDING_PICKUPstatus. - Traffic delay is a heuristic, not live data.
TrafficEstimationServiceapplies a simple distance + rush-hour-window multiplier. There's no integration with a real traffic API; treat the routing engine's cost output as directionally useful, not authoritative. DispatchService.dispatch(...)mutates thePatientargument's fields and relies on Hibernate's dirty-checking to flush at commit : the caller (DispatchController) fetches thePatientviaPatientRepositoryinside the same request, so it's managed by the persistence context. Don't calldispatch(...)with a detachedPatientinstance from outside a transactional, repository-backed context.
Apache License 2.0 : see LICENSE. The dev JWT signing keypair under scripts/dev-auth/ is
excluded from that concern entirely: it's disposable test material, not a secret to protect,
precisely because it's committed in the open. Rotate it (or replace with a real IdP) before any
non-local use.
This has been run for real, end to end, on a live Windows machine : not just statically checked:
mvn clean install: full multi-module build against real Maven Central dependencies: passes.mvn test: bothDijkstraRoutingEngineTest(plain JUnit, all routing/cost-weighting assertions) andTenantIsolationTest(realpostgis/postgis:16-3.4Testcontainers instance, real Flyway migrations, real PostGIS spatial queries): passes.mvn spring-boot:runagainst thedocker-compose.ymlstack : Flyway migrations, Hibernate + hibernate-spatial initialization, Kafka listener partition assignment, and a live authenticated REST request (GET /api/v1/dispatch/activewith a real signed JWT) all confirmed working.
Before that, an earlier pass did static-only verification (javac parsing without real
dependencies, manual cross-file signature tracing) since that environment had no Maven Central or
Docker access. That pass caught everything except one thing a real compiler ultimately caught: a
wrong import package for AutoConfigureTestDatabase : now fixed. Getting from "compiles
statically" to "actually runs" surfaced the environment-level issues cataloged in
Troubleshooting above (none were code bugs : Docker/Postgres/port configuration on the host
machine, plus one real Testcontainers/Docker-Desktop version incompatibility that's now pinned
correctly in pom.xml).