Frontend Development¶
The frontend is packages/frontend (@ronl/frontend) — a React 18 + TypeScript SPA built with Vite.
Project structure¶
packages/frontend/src/
├── App.tsx # Root component, environment detection, layout
├── main.tsx # React entry point, StrictMode
├── index.css # Global CSS (Tailwind base + custom properties)
├── components/ # UI components
├── contexts/ # React contexts (auth, tenant)
├── hooks/ # Custom React hooks
├── services/
│ ├── keycloak.ts # Keycloak JS adapter initialisation
│ ├── api.ts # Business API HTTP client (Axios)
│ └── tenant.ts # Tenant config loading and theme application
└── themes/ # Per-municipality theme tokens
packages/frontend/public/
└── tenants.json # Municipality configurations (loaded at runtime)
Authentication with Keycloak JS¶
services/keycloak.ts initialises the Keycloak JS adapter on app start:
const keycloak = new Keycloak({
url: import.meta.env.VITE_KEYCLOAK_URL,
realm: 'ronl',
clientId: 'ronl-business-api',
});
The adapter checks for an existing session token. If none is found, it redirects the user to Keycloak automatically. On successful authentication, keycloak.token holds the JWT access token, which is included in all subsequent API calls.
Token refresh is handled automatically by the adapter before the 15-minute expiry.
Multi-tenant theming¶
On successful login, services/tenant.ts reads the municipality claim from the decoded JWT and applies the corresponding theme:
initializeTenantTheme loads public/tenants.json, finds the matching entry, and calls applyTenantTheme, which sets CSS custom properties on document.documentElement:
root.style.setProperty('--color-primary', theme.primary);
root.style.setProperty('--color-primary-dark', theme.primaryDark);
// ...
All Tailwind utility classes and component styles reference these custom properties, so the entire UI re-themes without a page reload.
API client¶
services/api.ts wraps Axios and adds the JWT bearer token to every request:
const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
client.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${keycloak.token}`;
return config;
});
If a request returns HTTP 401 (token expired between refresh cycles), the interceptor triggers a silent Keycloak refresh and retries.
Environment variables¶
| Variable | Description | Example |
|---|---|---|
VITE_API_URL |
Business API base URL | http://localhost:3002/v1 |
VITE_KEYCLOAK_URL |
Keycloak base URL | http://localhost:8080 |
Three .env files are used for different deployments:
- .env — local development
- .env.acceptance — ACC deployment
- .env.production — production deployment
The Vite build command specifies which file to use: vite build --mode acceptance.
Development commands¶
npm run dev # Vite dev server with HMR on http://localhost:5173
npm run build # Production build → dist/
npm run build:acc # Acceptance build (uses .env.acceptance)
npm run build:prod # Production build (uses .env.production)
npm run lint # ESLint
npm run lint:fix # ESLint with auto-fix
npm run type-check # tsc --noEmit
Calling the Business API from a component¶
services/api.ts exposes two primary methods. Use these when adding new features that interact with the backend:
Evaluate a DMN decision:
import { businessApi } from '../services/api';
const result = await businessApi.evaluateDecision('berekenrechtenhoogtezorg', {
inkomenEnVermogen: { value: 24000, type: 'Integer' },
heeftZorgverzekering: { value: true, type: 'Boolean' },
});
Start a BPMN process:
const result = await businessApi.startProcess('vergunning', {
aanvrager: { value: 'Test Burger', type: 'String' },
adres: { value: 'Teststraat 123, Utrecht', type: 'String' },
});
console.log('Process started:', result.processInstanceId);
Both methods attach the JWT bearer token automatically via the Axios interceptor and return the parsed ApiResponse<T> from @ronl/shared. Wrap calls in try/catch — on HTTP 401 the interceptor attempts a silent Keycloak token refresh before rejecting.
Adding a new page¶
- Create a component in
src/components/ - Add a route in
App.tsx - If the page requires authentication, wrap it with the auth context guard
- Apply Tailwind classes using
var(--color-primary)for municipality-branded colours