Environment Variables¶
Backend โ packages/backend/.env¶
Server¶
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV |
Yes | โ | development, acceptance, or production |
PORT |
Yes | 3002 |
HTTP listen port (Azure uses 8080) |
HOST |
No | 0.0.0.0 |
Bind address |
CORS¶
| Variable | Required | Description |
|---|---|---|
CORS_ORIGIN |
Yes | Comma-separated allowed origins (e.g. https://mijn.open-regels.nl), matched by exact equality |
CORS_PREVIEW_SLUGS |
No | Comma-separated Static Web Apps app slugs whose numbered preview environments may call this backend. Empty by default, and ignored outright in production |
CORS_PREVIEW_SLUGS holds slugs, not hostnames
A pull-request preview gets an ephemeral origin, and there is a new one per
pull request, so it cannot be listed in CORS_ORIGIN. Azure derives a
preview hostname from the app's stable slug, so the pattern anchors on
the slug โ a bare *.azurestaticapps.net pattern would let any Azure Static
Web App in the world make credentialed cross-origin requests to the tier.
Two properties are enforced in code rather than left to configuration. It is
never honoured in production, so a value that finds its way onto the
production App Service changes nothing about what production accepts. And
the guard reads the deployment environment, not NODE_ENV: acceptance
deliberately runs NODE_ENV=production, so keying on that would have
treated it as production and refused every preview.
Keycloak / JWT¶
| Variable | Required | Description |
|---|---|---|
KEYCLOAK_URL |
Yes | Keycloak base URL (e.g. https://keycloak.open-regels.nl) |
KEYCLOAK_REALM |
Yes | Realm name โ always ronl |
KEYCLOAK_CLIENT_ID |
Yes | Client ID โ always ronl-business-api |
JWT_ISSUER |
Yes | Full issuer URL: https://keycloak.open-regels.nl/realms/ronl |
JWT_AUDIENCE |
Yes | Must match token aud claim โ always ronl-business-api |
TOKEN_CACHE_TTL |
No | 300 |
There is no KEYCLOAK_CLIENT_SECRET
ronl-business-api is a public client: the realm export gives it
publicClient: true, no secret and no service account. The setting existed
on Config until v2026.09.10, was populated from the environment, was
required in production โ and was read by no code path. Requiring a value
nobody reads is how the production App Service came to hold the literal
not-used. It was removed rather than corrected.
An unfilled value fails the boot in production
A non-empty check passes a placeholder, so the backend boots, /v1/health
reports healthy, and the breakage surfaces at first use. Since v2026.09.10 a
boot-time check rejects an unfilled value in production as well as an empty
one, and ANTHROPIC_API_KEY uses it โ that key is genuinely consumed.
Matching is anchored, never by substring: exchange-mechanism-2026
contains change-me, and failing a boot over a legitimate secret would be a
worse failure than the one this prevents. Production only, because failing a
developer's boot over an unfilled .env would be hostile.
Operaton¶
| Variable | Required | Description |
|---|---|---|
OPERATON_BASE_URL |
Yes | https://operaton.open-regels.nl/engine-rest |
OPERATON_TIMEOUT |
30000 |
Operaton request timeout in ms |
MCP AI Assistant¶
| Variable | Required | Default | Description |
|---|---|---|---|
MCP_ENABLED |
No | false |
Enables the MCP client and POST /v1/mcp/chat. Must be true on ACC/PROD. |
MCP_SKIP_HEALTH_CHECK |
No | false |
Skips provider health checks on startup. Useful when providers start slowly on first deployment. |
ANTHROPIC_API_KEY |
Yes | โ | Anthropic API key. Required in every environment, whatever MCP_ENABLED says: the boot fails without it, and in production also when it is still a placeholder value. |
OPENAI_API_KEY |
No | โ | Enables OpenAILlmProvider and exposes gpt-4o and gpt-4o-mini in the model selector. Leave unset to use Anthropic only. Requires the openai package: npm install openai --workspace=@ronl/backend. |
TRIPLYDB_MCP_ENABLED |
No | false |
Enables the TriplyDB Knowledge Graph MCP provider. |
TRIPLYDB_ENDPOINT |
Conditional | โ | SPARQL endpoint URL. Required when TRIPLYDB_MCP_ENABLED=true. Use https://api.open-regels.triply.cc/datasets/stevengort/RONL/services/RONL/sparql for the canonical RONL graph. |
TRIPLYDB_TOKEN |
No | โ | TriplyDB API token. May be empty for public datasets. |
CPRMV_MCP_ENABLED |
No | false |
Enables the CPRMV legislation provider (Dutch and EU law via HTTP MCP). |
CPRMV_URL |
No | https://acc.cprmv.open-regels.nl/mcp |
CPRMV MCP server URL. Override for PROD deployment. |
LDE_MCP_ENABLED |
No | false |
Enables the LDE Process Library provider. Exposes deployed BPMN bundles, form schemas, and document templates to the AI Assistant. |
LDE_DATABASE_URL |
Conditional | โ | PostgreSQL connection string for the lde_assets database. Required when LDE_MCP_ENABLED=true. On Azure: use a separate Flexible Server and append ?sslmode=require. Locally: reuse the existing ronl-postgres container. |
OPERATON_USERNAMEandOPERATON_PASSWORDare also passed to theoperaton-mcpchild process. Ensure they are set before enabling MCP.
Operaton โ M2M¶
| Variable | Required | Default | Description |
|---|---|---|---|
OPERATON_M2M_BASE_URL |
No | โ | Override: a different Operaton engine for the M2M routes. Unset โ as on every tier โ /v1/m2m uses the main engine, OPERATON_BASE_URL |
OPERATON_M2M_USERNAME |
No | โ | Basic auth username for the override engine; when unset, OPERATON_USERNAME is used, so set it together with the URL |
OPERATON_M2M_PASSWORD |
No | โ | Basic auth password for the override engine; when unset, OPERATON_PASSWORD is used |
M2M_ALLOWED_CLIENTS |
No | operaton-mcp-client |
Comma-separated Keycloak client ids allowed to call /v1/m2m, matched against the token's azp. A token from any other client โ every person's token included โ is refused with 403 M2M_CLIENT_NOT_ALLOWED. Adding a consumer means adding its client id here; nothing changes in Keycloak |
eDOCS¶
With EDOCS_STUB_MODE=false the backend refuses to start unless EDOCS_USER_ID, EDOCS_PASSWORD, ENTRA_TENANT_ID, ENTRA_CLIENT_ID and ENTRA_CLIENT_SECRET are set. A person works in eDOCS as themselves, with the Entra ID token Keycloak stored at their login; the service account serves machine clients and background archiving โ see eDOCS โ Live Testing.
| Variable | Required | Default | Description |
|---|---|---|---|
EDOCS_STUB_MODE |
No | true |
When true, all eDOCS service methods return realistic fake responses. Set to false to enable live calls. Never commit real credentials to the repository โ use Azure App Service Application settings. |
EDOCS_BASE_URL |
Yes (live mode) | โ | eDOCS REST API root, e.g. https://<host>:<port>/edocsapi/v1.0. The client appends connect, workspaces, documents and libraries, so a trailing /connect makes every call fail. Not checked at startup |
EDOCS_LIBRARY |
No | DOCUVITT |
eDOCS library name |
EDOCS_USER_ID |
Yes (live mode) | โ | The service account's user ID (testuser001), used by machine clients and background archiving. The backend refuses to start without it in live mode |
EDOCS_PASSWORD |
Yes (live mode) | โ | The service account's password. The backend refuses to start without it in live mode; every failed login counts towards an eDOCS lockout |
EDOCS_DEPARTMENT |
No | IVR |
The UV_AFD_NAAM profile field set on every uploaded document, which the DM server requires and validates against its own department list. A property of the eDOCS environment, not of a process; an invalid value makes every upload fail with 502 |
ENTRA_TENANT_ID |
Yes (live mode) | โ | Provincie Flevoland's Entra tenant, for refreshing a person's brokered Entra ID token |
ENTRA_CLIENT_ID |
Yes (live mode) | โ | The app registration Keycloak brokers (IOU-demonstrator) |
ENTRA_CLIENT_SECRET |
Yes (live mode) | โ | Its client secret โ the same value Keycloak's entra-flevoland provider holds |
ENTRA_IDP_ALIAS |
No | entra-flevoland |
The Keycloak identity provider whose stored tokens the backend reads |
EDOCS_ALLOW_SERVICE_FALLBACK |
No | false |
true lets a person without a stored Entra token (a Keycloak test account) fall back to the service account, visibly (actingAs: "service", with an audit entry). Refused at startup when DEPLOYMENT_ENV=production |
EDOCS_ALLOWED_CLIENTS |
No | edocs-mcp-client,copilot-studio-edocs,operaton-mcp-client |
Comma-separated Keycloak client ids (token azp) allowed on /v1/edocs; they act as the service account. Any other machine client is refused with 403 EDOCS_CLIENT_NOT_ALLOWED |
EDOCS_MCP_ENABLED |
No | false |
Enables the AI assistant's eDOCS tools (.env.example sets true) |
EDOCS_MCP_CLIENT_ID |
No | edocs-mcp-client |
The Keycloak client the eDOCS MCP subprocess authenticates as when a call carries no person |
EDOCS_MCP_CLIENT_SECRET |
Conditional | โ | Its client secret; needed when EDOCS_MCP_ENABLED=true |
GitLab integration¶
| Variable | Default | Description |
|---|---|---|
GITLAB_TOKEN |
โ | Personal access token with api scope for the GitLab instance |
GITLAB_BASE_URL |
https://git.open-regels.nl |
GitLab instance base URL |
GITLAB_PROJECT_PATH |
โ | URL-encoded project path (e.g. showcases%2Fiou-architectuur) |
GITLAB_UC_LABEL |
uc::submitted |
Label applied to newly created use-case issues |
Database (PostgreSQL)¶
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Full connection string with ?sslmode=require in production |
DATABASE_POOL_MIN |
No | 2 |
DATABASE_POOL_MAX |
No | 10 |
Redis¶
| Variable | Required | Description |
|---|---|---|
REDIS_URL |
Yes | Redis connection string |
REDIS_TTL |
No | 3600 |
Rate limiting¶
| Variable | Required | Default | Description |
|---|---|---|---|
RATE_LIMIT_WINDOW_MS |
No | 60000 |
Rate limit window in ms |
RATE_LIMIT_MAX_REQUESTS |
No | 1000 |
Max requests per window |
RATE_LIMIT_PER_TENANT |
No | true |
Asks for the limit to be keyed per tenant and IP. Has no effect: the limiter runs before any route authenticates the caller, so there is never a tenant to key on and every bucket is per client IP |
The limit buckets per client, which is why acceptance could raise it
TRUST_PROXY is true on both deployed tiers, so the limiter buckets per
client rather than once per deployment. Acceptance runs
RATE_LIMIT_MAX_REQUESTS=1000, raised from 100 so a full end-to-end run
from one machine stops throttling in the PA cockpit specs, which spend about
twenty requests per authoring journey.
Production should not follow automatically. Per-client bucketing is what makes a raise a convenience decision on acceptance; a tenfold ceiling per client is a much weaker defence on a public tier.
Logging¶
| Variable | Required | Default | Description |
|---|---|---|---|
LOG_LEVEL |
No | info |
debug, info, warn, error |
LOG_FORMAT |
No | json |
json (production) or pretty (local) |
LOG_FILE_ENABLED |
No | true |
Write logs to rotating files |
LOG_FILE_PATH |
No | ./logs |
Log file directory |
LOG_FILE_MAX_SIZE |
No | 10m |
Max log file size before rotation |
LOG_FILE_MAX_FILES |
No | 7 |
Number of rotated log files to keep |
Audit logging¶
| Variable | Required | Default | Description |
|---|---|---|---|
AUDIT_LOG_ENABLED |
No | true |
Enable audit log writes |
AUDIT_LOG_INCLUDE_IP |
No | true |
Include client IP in audit records |
AUDIT_LOG_RETENTION_DAYS |
No | 2555 |
Parsed into Config and read by no code: nothing purges audit records |
Security¶
| Variable | Required | Default | Description |
|---|---|---|---|
HELMET_ENABLED |
No | true |
Enable Helmet security headers |
SECURE_COOKIES |
No | false |
Set Secure flag on cookies (enable in prod) |
TRUST_PROXY |
No | false |
Trust Azure/proxy X-Forwarded-* headers (enable in prod) |
Features¶
| Variable | Required | Default | Description |
|---|---|---|---|
ENABLE_TENANT_ISOLATION |
No | true |
Switches the tenant middleware's presence check (403 MISSING_TENANT for a token without a tenant). It does not switch tenant access: the TENANT_MISMATCH checks on process instances and tasks always run |
ENABLE_METRICS |
No | true |
Parsed into Config and read by no code; no metrics endpoint is served |
ENABLE_HEALTH_CHECKS |
No | true |
Parsed into Config and read by no code; /v1/health is always served |
DEFAULT_MAX_PROCESS_INSTANCES |
No | 1000 |
Parsed into Config and read by no code; no per-tenant instance limit is applied |
RONL_SPARQL_ENDPOINT |
No | https://api.triplydb.com/... |
Override the default RONL TriplyDB SPARQL endpoint used by the Regelcatalogus service |
Media aggregator¶
The backend both serves a media aggregator at /v1/media-aggregator and, for the policy-analysis cockpit, consumes one as a media source. The two halves are configured separately.
| Variable | Required | Default | Description |
|---|---|---|---|
MEDIA_SOURCE_ENABLED |
No | false |
Adds media as a policy-analysis source: curation and searches query the aggregator at MEDIA_AGGREGATOR_BASE |
MEDIA_AGGREGATOR_BASE |
When MEDIA_SOURCE_ENABLED=true |
โ | Base URL of the aggregator the policy-analysis media source calls (<base>/search) |
MEDIA_AGGREGATOR_API_KEY |
When the aggregator requires a key | โ | Sent as Authorization: Bearer by the policy-analysis media source |
MEDIA_AGGREGATOR_ACCEPT_KEY |
No | โ | Guards GET /v1/media-aggregator/search: when set, a caller must send it as a bearer token, and anything else gets 401; when unset, the endpoint is open. This is the mediaAggregatorKey security scheme in the OpenAPI description. Set it to the same value as MEDIA_AGGREGATOR_API_KEY when the backend consumes its own aggregator |
MEDIA_AGGREGATOR_CACHE_TTL_MS |
No | 900000 |
How long the aggregator's in-memory article set is served before a refresh (15 minutes). A missing or non-positive value uses the default |
MEDIA_AGGREGATOR_SENTIMENT_ENABLED |
No | โ | Has no effect: sentiment analysis is not implemented, and every article's sentiment is null whatever this says |
Public surface¶
| Variable | Required | Default | Description |
|---|---|---|---|
PUBLIC_PROCESS_BOARDS |
No | caseworker |
Comma-separated list of boardOwner values whose process bundles are exposed on the public process library and its search. Bundles carrying no owner at all are always public. Widen it (e.g. caseworker,infra-board) to publish another board's bundles. A bundle's status label plays no part in this โ see Procesbibliotheek |
Frontend โ packages/frontend/.env.<mode>¶
The frontend's variables are not secrets: they are committed per build mode in packages/frontend/.env.development, .env.acceptance, .env.production and .env.test, and Vite reads the file for the mode it builds with. The ACC deploy and its pull-request previews both build with --mode acceptance, so a preview carries the acceptance values.
| Variable | Required | Description |
|---|---|---|
VITE_API_URL |
Yes | Business API base URL (e.g. https://api.open-regels.nl/v1) |
VITE_KEYCLOAK_URL |
Yes | Keycloak base URL (e.g. https://keycloak.open-regels.nl) |
VITE_LDE_API_URL |
Yes | LDE public API base URL. Used by ProcesBibliotheek to fetch deployed BPMN bundles. ACC: https://acc.backend.linkeddata.open-regels.nl/v1. PROD: https://backend.linkeddata.open-regels.nl/v1. |
VITE_SITE_URL |
Yes | The site's own origin. Fills the canonical link, og:url and the absolute og:image URL in index.html |
VITE_OG_IMAGE |
Yes | Link-preview image file under public/, appended to VITE_SITE_URL |
VITE_OG_TITLE_PREFIX |
No | Prefix for og:title, telling a non-production preview apart. Quoted in the file, or the trailing space is lost |
VITE_ROBOTS |
Yes | Content of the robots meta tag |
The link-preview values per mode (.env.test carries the development values):
| Variable | Development | Acceptance | Production |
|---|---|---|---|
VITE_SITE_URL |
http://localhost:5173 |
https://acc.mijn.open-regels.nl |
https://mijn.open-regels.nl |
VITE_OG_IMAGE |
og-image-acc.png |
og-image-acc.png |
og-image-prod.png |
VITE_OG_TITLE_PREFIX |
"[DEV] " |
"[ACC] " |
"" |
VITE_ROBOTS |
noindex, nofollow |
noindex, nofollow |
index, follow |
Public site โ packages/public-site/.env.<mode>¶
Committed per build mode in the same way, and here too the ACC deploy and its pull-request previews build with --mode acceptance. scripts/prerender.ts reads VITE_SITE_URL, VITE_API_URL and VITE_ROBOTS from the same file, and fails the build when one is missing. There is no VITE_OG_IMAGE: every environment uses the one og-open-regels.png.
| Variable | Development | Acceptance | Production | Description |
|---|---|---|---|---|
VITE_API_URL |
http://localhost:3002/v1 |
https://acc.api.open-regels.nl/v1 |
https://api.open-regels.nl/v1 |
Business API base URL for the public endpoints |
VITE_STAFF_APP_URL |
http://localhost:5173 |
https://acc.mijn.open-regels.nl |
https://mijn.open-regels.nl |
Target of the sign-in link to the working environment |
VITE_SITE_URL |
http://localhost:5175 |
https://acc.publiek.open-regels.nl |
https://publiek.open-regels.nl |
The site's own origin: canonical links, og:url, og:image, the sitemap and the footer |
VITE_OG_TITLE_PREFIX |
"[DEV] " |
"[ACC] " |
"" |
Prefix for og:title; quoted to keep the trailing space |
VITE_ROBOTS |
noindex, nofollow |
noindex, nofollow |
index, follow |
The robots meta tag; a noindex value also makes the prerender write a robots.txt that disallows everything and names no sitemap |
DNS records¶
These CNAME records must exist in the open-regels.nl DNS zone before deploying:
# ACC
acc.api CNAME ronl-business-api-acc.azurewebsites.net
acc.mijn CNAME <acc-static-web-app>.azurestaticapps.net
# PROD
api CNAME ronl-business-api-prod.azurewebsites.net
mijn CNAME <prod-static-web-app>.azurestaticapps.net
VM subdomains use A records pointing to the VM's public IP:
GitHub repository secrets and variables¶
The static sites deploy with a token, which is a secret:
| Secret name | Where to get it |
|---|---|
AZURE_STATIC_WEB_APPS_API_TOKEN_ACC |
Azure Portal โ Static Web App ACC โ Manage deployment token |
AZURE_STATIC_WEB_APPS_API_TOKEN_PROD |
Azure Portal โ Static Web App PROD โ Manage deployment token |
AZURE_STATIC_WEB_APPS_API_TOKEN_PA_DEMO_ACC / _PROD |
Azure Portal โ the PA-demo Static Web App โ Manage deployment token |
AZURE_STATIC_WEB_APPS_API_TOKEN_PUBLIC_SITE_ACC / _PROD |
Azure Portal โ the public-site Static Web App โ Manage deployment token |
The backend deploys over OIDC and therefore needs no secret at all. What it reads are repository variables:
| Variable name | Value |
|---|---|
AZURE_CLIENT_ID_ACC / AZURE_CLIENT_ID_PROD |
The app registration with a federated credential for that App Service โ one per tier, each scoped to its own |
AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID |
The tenant and subscription the App Services live in |
AZURE_WEBAPP_PUBLISH_PROFILE_ACC / _PROD are dead
Those secrets date from March 2026 and nothing reads them. SCM basic auth is disabled on both App Services, so a publish-profile deploy would be rejected โ which is why the backend authenticates with OIDC instead.
Store a token with scripts/set-secret.sh, not by piping
Piping a token straight out of the Azure CLI into gh secret set stores a
trailing newline โ 120 bytes where the key is 119. Both halves are the
documented way to do their job; the composition is what goes wrong, and it
cost the public site its first production deploy. The failure named nothing:
every step passed, then "An unknown exception has occurred".
A secret's value cannot be read back, so nothing can confirm or deny a stray newline afterwards. The script strips whitespace, refuses an empty result, and reports the byte count it stored.
Generating environment passwords¶
Save and run this script locally to generate all secrets for an environment:
#!/bin/bash
set -e
ENV=${1:-acc} # usage: ./setup-env.sh acc OR ./setup-env.sh prod
POSTGRES_PASSWORD=$(openssl rand -base64 32)
KEYCLOAK_PASSWORD=$(openssl rand -base64 32)
mkdir -p ~/.ronl-secrets
cat > ~/.ronl-secrets/${ENV}-passwords.txt << EOF
# RONL ${ENV^^} Environment โ Generated: $(date)
PostgreSQL:
Username: pgadmin
Password: ${POSTGRES_PASSWORD}
Keycloak Admin:
Username: admin
Password: ${KEYCLOAK_PASSWORD}
Connection strings:
DATABASE_URL: postgresql://pgadmin:${POSTGRES_PASSWORD}@ronl-postgres-${ENV}.postgres.database.azure.com:5432/audit_logs?sslmode=require
Keycloak VM .env: KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_PASSWORD}
EOF
chmod 600 ~/.ronl-secrets/${ENV}-passwords.txt
echo "Passwords saved to: ~/.ronl-secrets/${ENV}-passwords.txt"
echo "Back this file up securely before proceeding."