Troubleshooting¶
Diagnosing a problem¶
Start by identifying which layer the error comes from:
| Where to look | What it covers |
|---|---|
| Browser console (F12 โ Console) | Frontend JavaScript errors |
| Browser network tab (F12 โ Network) | API call failures, CORS errors, HTTP status codes |
Terminal running npm run dev |
Backend errors, stack traces |
docker compose logs -f keycloak |
Keycloak startup, authentication errors |
docker compose logs -f postgres |
Database connection errors |
docker compose logs -f operaton |
Local engine startup, deployment and process errors |
Authentication errors¶
JWT audience invalid¶
The access token is missing the aud claim. Locally, a realm imported from the
current config/keycloak/ronl-realm.json already carries this mapper and the
realm_roles mapper below; see
Local Development โ Common issues for
resetting an older realm. On an environment you cannot reset, add the Audience
mapper in Keycloak Admin:
- Open Keycloak Admin:
http://localhost:8080 - Select realm ronl
- Navigate to Clients โ ronl-business-api โ Client scopes โ ronl-business-api-dedicated
- Click Add mapper โ By configuration โ Audience
- Set:
- Name:
audience - Included Client Audience:
ronl-business-api - Add to access token: ON
- Save, then log out and log back in
Verify by decoding your token at jwt.io and checking for "aud": "ronl-business-api".
Roles not appearing in JWT¶
All users show as citizen even though a test user is configured as caseworker.
The realm_roles protocol mapper is missing. Add it in Keycloak Admin:
- Clients โ ronl-business-api โ Client scopes โ ronl-business-api-dedicated โ Mappers
- If
realm_rolesis missing, click Add mapper โ By configuration โ User Realm Role - Set:
- Name:
realm_roles - Multivalued: ON
- Token Claim Name:
realm_access.roles - Add to access token: ON
- Save, then log out and log back in
Login redirects to blank page¶
Cause: Invalid redirect URIs in the Keycloak client.
- Clients โ ronl-business-api โ Settings
- Set Valid Redirect URIs to
* - Set Valid Post Logout Redirect URIs to
* - Set Web Origins to
+ - Save
Token expired¶
This is expected โ access tokens have a 15-minute lifetime. Log out and log back in. Token refresh is a planned production feature.
CORS errors¶
CORS policy blocking Keycloak¶
Access to XMLHttpRequest at 'http://localhost:8080/realms/ronl/...'
from origin 'http://localhost:5173' has been blocked by CORS policy
- Clients โ ronl-business-api โ Settings
- Set Web Origins to
+(inherits all valid redirect URIs) - Save
If the error persists, verify CORS_ORIGIN in packages/backend/.env matches http://localhost:5173 exactly (no trailing slash).
Procesbibliotheek CORS error¶
Symptom: The Procesbibliotheek section is empty and the browser console shows a CORS policy error for a request to https://backend.linkeddata.open-regels.nl/v1/bundles/public from origin https://mijn.open-regels.nl.
Cause: Procesbibliotheek calls the standalone LDE backend directly from the browser (VITE_LDE_API_URL/bundles/public), not via the business API. The LDE backend is a separate deployment with its own ACC/PROD environment split and its own CORS allowlist. A new frontend origin is not automatically allowed.
Fix: Add the frontend origin (e.g. https://mijn.open-regels.nl) to the LDE backend's CORS allowlist โ wherever that backend configures it (env var or its own cors() config) โ then restart it. Confirm with:
curl -s -i -X OPTIONS https://backend.linkeddata.open-regels.nl/v1/bundles/public \
-H "Origin: https://mijn.open-regels.nl" \
-H "Access-Control-Request-Method: GET" | grep -i 'access-control'
The response must include Access-Control-Allow-Origin: https://mijn.open-regels.nl. The rest of the dashboard is unaffected by this โ only Procesbibliotheek degrades.
Backend API errors¶
Health check returns unhealthy dependencies¶
The endpoint answers 200 with data.status "healthy" when Keycloak and
Operaton are both up, and 503 with "degraded" otherwise:
{
"success": false,
"data": {
"status": "degraded",
"dependencies": {
"keycloak": { "status": "up", "latency": 9 },
"operaton": { "status": "down", "error": "<the connection error>" },
"cache": { "status": "up" }
}
}
}
The full response shape is on Local Development โ Verifying the setup.
If keycloak is "down": check docker compose ps and that ronl-keycloak is
healthy and listening on port 8080.
If operaton is "down": locally the engine is the ronl-operaton container.
Check docker compose ps and docker compose logs operaton, and check that
OPERATON_BASE_URL in packages/backend/.env is
http://localhost:8081/engine-rest. Without that key the backend falls back to
the shared remote engine.
If cache is "down": Redis (ronl-redis) is not reachable. This does not
make the check fail; the PA monitoring cache falls back to live fetches.
Port already in use¶
Find and stop whatever is occupying the port:
# Linux/Mac
lsof -i :3002
kill -9 <PID>
# Windows
netstat -ano | findstr :3002
taskkill /PID <PID> /F
Alternatively, change the port in packages/backend/.env:
And point VITE_API_URL at it in packages/frontend/.env.development.local
(gitignored; it overrides the committed .env.development). Add
http://localhost:<port> origins to CORS_ORIGIN if you move a front end.
API returns 500 Internal Server Error¶
Check the backend terminal for a stack trace. Common causes:
- Operaton unreachable (
OPERATON_BASE_URLwrong or service down) - Invalid request body (missing required fields)
- Missing environment variable (check
packages/backend/.envis populated)
Zorgtoeslag calculation fails¶
DMN hit policy violation¶
Symptom:
POST /v1/decision/berekenrechtenhoogtezorg/evaluate โ 500
{
"success": false,
"error": {
"code": "DECISION_EVALUATION_FAILED",
"message": "DMN configuratiefout in beslissingstabel 'berekenrechtenhoogtezorg': meerdere regels zijn tegelijk van toepassing, maar het trefriebeleid (hit policy) staat slechts รฉรฉn treffer toe. Neem contact op met de beheerder."
}
}
Cause: The DMN decision table uses the default UNIQUE hit policy, which requires exactly one rule to match per evaluation. When multiple disqualifying conditions are true simultaneously (e.g. both betalingsregeling = true and detentie = true), two rules match and Operaton throws a runtime exception. Operaton surfaces this as "Exception while evaluating decision with key 'null'" โ the 'null' refers to the internal rule key that could not be resolved, not to the decision key itself.
Fix: Open BerekenRechtEnHoogteZorg.dmn in Camunda Modeler and set the hit policy on the decision table to FIRST:
With FIRST, rules are evaluated top-to-bottom and evaluation stops at the first match. Ensure the disqualifying rules (betalingsregeling, detentie, ingezetene, leeftijd, verzekering) appear above the income threshold rules and the positive allowance rule.
Also correct the Null literals in all disqualifying output entries โ the FEEL spec requires lowercase null:
<!-- Wrong -->
<outputEntry><text>Null</text></outputEntry>
<!-- Correct -->
<outputEntry><text>null</text></outputEntry>
After editing, redeploy the DMN to Operaton via the Camunda Modeler deploy feature or the Operaton REST API.
Error message routing: The operaton.service.ts catch block detects this specific error pattern and throws a descriptive Error instead of re-throwing the raw axios exception. decision.routes.ts propagates that message as error.message in the 500 response body. The frontend api.ts reads error.response.data on a 500 and returns it as a structured ApiResponse, so Dashboard.tsx renders result.error.message directly. Citizens see a neutral notification; caseworkers see the technical message.
Other causes¶
- Open browser DevTools โ Network tab
- Find the request to
/v1/decision/berekenrechtenhoogtezorg/evaluate - Check the response body โ the
error.messagefield identifies the cause - Common causes:
audclaim missing from token (see JWT audience fix above), Operaton service unreachable (check health endpoint), invalid input variable types
Test user issues¶
Can't log in with test users¶
Verify the users exist in Keycloak:
- Open Keycloak Admin โ Users
- Search for
test-citizen-utrecht - The realm holds 22 test users across eight tenants, all with password
test123; see Local Development โ Test users
If the users are missing, the realm import did not run, or ran from an older
realm file. Keycloak stores the realm in Postgres and skips the import when a
ronl realm already exists, so removing only keycloak-data does not bring it
back. Reset all volumes:
npm run docker:down:volumes
npm run docker:up
# Wait up to a minute for Keycloak to become healthy and import the realm
npm run dev
This also clears the audit database, Redis and the Operaton deployments, so deploy the fixtures again.
Browser issues¶
Changes not visible after saving¶
Vite HMR should update automatically. If it doesn't:
If that fails: F12 โ Application โ Clear storage โ Clear site data โ reload.
Regular Chrome not working, Incognito works¶
Cause: Browser cache holding stale CORS errors.
- Close all Chrome windows completely
- Reopen Chrome
- Try again
If still broken, use Incognito for the remainder of the session. This is a browser cache issue, not a code issue.
Windows-specific issues¶
Line ending warnings¶
The repository's .gitattributes sets * text=auto eol=lf, which keeps text
files LF on checkout and commit whatever your core.autocrlf says. You do not
need to change your Git configuration for this repository. If a file still shows
up as modified with only line-ending changes after you pull the .gitattributes
change, run git add --renormalize . once.
bash not found, or scripts running in WSL¶
npm ci, npm run dev and the checks call bash. See
Local Development โ A bash shell is required.
Emergency reset¶
When everything is broken and you want a clean slate:
# 1. Stop dev servers
Ctrl+C
# 2. Wipe Docker: containers and all four volumes
# (Keycloak realm, audit database, Redis, Operaton deployments)
npm run docker:down:volumes
# 3. Reinstall exactly what package-lock.json records
# (npm ci deletes node_modules itself)
npm ci
npm run build --workspace=@ronl/shared
# 4. Start
npm run docker:up
# Wait until docker compose ps shows the containers healthy
npm run dev
After this the local Operaton engine is empty. Deploy the process and decision fixtures again before starting processes; see Local Development โ The engine starts empty.
Diagnostic commands¶
# Check tool versions
node --version
npm --version
docker --version
# Check running containers
docker compose ps
docker compose logs keycloak --tail=50
# Check ports in use (Linux/Mac)
sudo lsof -i :5173 # frontend
sudo lsof -i :3002 # backend
sudo lsof -i :8080 # keycloak
# Check ports in use (Windows)
netstat -ano | findstr :5173
netstat -ano | findstr :3002
netstat -ano | findstr :8080
# API health with formatted output
curl http://localhost:3002/v1/health | jq
# Keycloak: is the ronl realm served?
curl http://localhost:8080/realms/ronl/.well-known/openid-configuration
# Operaton engine version
curl -u demo:demo http://localhost:8081/engine-rest/version