Local Development¶
This guide covers running the CPSV Editor locally, the Linked Data Explorer backend it talks to, and the two Docker containers from the RONL Business API stack that backend needs.
When you need a local backend¶
In development mode the editor sends every backend call to http://localhost:3001, the
Linked Data Explorer (LDE) backend. Form editing, TTL generation, and TTL import and
export happen in the browser and work without it. These features call the backend:
| Feature | Backend route | Source |
|---|---|---|
| RONL analysis and method concepts, loaded on mount | POST /v1/triplydb/query |
src/utils/ronlHelper.js, src/hooks/useEditorState.js |
| SHACL validation | POST /v1/shacl/validate |
src/utils/shaclHelper.js |
| TriplyDB publishing โ the service-update step | POST /v1/triplydb/update-service |
src/utils/triplydbHelper.js |
| DMN validate, deploy and evaluate | POST /v1/dmns/validate, /v1/dmns/deploy, /v1/dmns/evaluate/{key} |
src/components/tabs/DMNTab.jsx |
DSO handoff from the LDE (?dsoImport=dmn) |
GET /v1/dso/toepasbare-regels/{id}/dmn |
src/hooks/useDsoImport.js |
Because the RONL concepts load as soon as the editor opens, starting the editor without a backend is not silent: the Legal tab reports "Failed to load concepts from TriplyDB" and the Vendor tab "Failed to load vendors from TriplyDB". Locally, the usual cause is not TriplyDB but a backend that is not running.
The same backend serves the Linked Data Explorer. Changing it for the editor can break the explorer, and the other way round.
The local stack¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Local Development Environment โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ CPSV Editor Linked Data Explorer frontend โ
โ http://localhost:3000 http://localhost:3000 โ
โ โ or :3002 alongside LDE (React / Vite) โ
โ (React / Vite) โ
โ โ โ โ
โ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ โ
โ โ โ
โ Linked Data Explorer backend โ
โ http://localhost:3001 โ
โ (Node.js + Express + TypeScript) โ
โ โ โ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ Docker (ronl-business-api stack) Remote services โ
โ โข ronl-operaton localhost:8081 โข TriplyDB โ
โ โข ronl-postgres localhost:5432 api.open-regels. โ
โ triply.cc โ
โ โข DSO APIs โ
โ (pre-production) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Operaton is not remote in local development. The LDE backend's
packages/backend/.env.example points OPERATON_BASE_URL at
http://localhost:8081/engine-rest, the ronl-operaton container from the
ronl-business-api stack, and the editor's .env.development sets VITE_OPERATON_URL to
http://localhost:8081. TriplyDB and the DSO APIs stay remote.
Prerequisites¶
| Tool | Version | Why |
|---|---|---|
| Node.js | 24.20.0 | .nvmrc. CI installs exactly this version (node-version-file: .nvmrc). The LDE repository pins its own version in its own .nvmrc, currently 24.21.0. |
| npm | 11.10 or newer | .npmrc sets a 14-day cooldown (min-release-age=14) that older npm ignores without a warning. Node 24.20.0 bundles npm 11.19. npm 10 also crashes on anything that re-resolves the tree (npm install <package>, npm update) โ see below. |
| bash | on PATH |
npm start runs npm run deps:check, which is bash scripts/check-deps.sh; the pre-push hook runs it too. On Windows, npm runs scripts through cmd.exe, so bash must be found on PATH โ Git for Windows provides one. |
| Git | โ | |
| Docker | โ | Only to run the LDE backend: its dev script refuses to start without the ronl-postgres and ronl-operaton containers. |
npm 10 crashes on re-resolving the tree
The README records that npm 10 (bundled with Node 22) installs from the lockfile with
npm ci, but crashes on npm install <package>, npm update, or regenerating the
lockfile, with Cannot read properties of null (reading 'edgesOut') โ a resolver bug
hit on vitest's optional peer chain. npm 11 resolves the same tree. Verified with
npm 10.9.4 and 11.19.1 on 11 September 2026.
scripts/check-deps.sh warns at every npm start and every push when npm is older than
11.10, and names npm install -g npm@11 as the fix. It is a warning, not a failure.
Install and start the editor¶
What each step does:
| Step | Effect |
|---|---|
npm ci |
Installs exactly what package-lock.json records. |
postinstall โ node scripts/write-deps-marker.mjs |
Copies the lockfile to node_modules/.package-lock-installed.json, the snapshot deps:check compares against. |
prepare โ husky |
Installs the Git hooks in .husky/. |
npm start โ npm run deps:check && vite |
Checks the install against the lockfile, then starts the Vite dev server on http://localhost:3000. It does not open a browser. |
deps:check compares the lockfile with the install marker, ignoring the repository's own
version number so a release bump alone does not count as a change. When they differ, or
node_modules or the marker is missing, it exits 1, prints npm ci as the fix, and Vite
does not start. A fast-forward brings lockfile changes in and installs nothing, so this is
what catches a stale install after a pull.
npm ci, not npm install
npm install re-resolves the version ranges. npm ci installs the lockfile as it is,
and ignores the .npmrc cooldown by design โ the lockfile is the source of truth. Use
npm install <package> only to add or upgrade one package. npm ci deletes
node_modules first, so stop the dev server before running it.
The port is fixed: vite.config.mjs sets server: { port: 3000, strictPort: true }. The
LDE backend allowlists browser origins for CORS by port, and strictPort stops Vite from
quietly moving to another port that the backend does not allow. If 3000 is taken, the
editor exits instead of starting.
Running alongside the Linked Data Explorer¶
Port 3000 is taken twice¶
The LDE frontend's dev server also uses port 3000 (packages/frontend/vite.config.ts).
With it running, npm start in the editor fails because of strictPort. The LDE's
packages/frontend/.env.development resolves this the other way round: it says to run
the CPSV Editor on a non-3000 port, and sets
That is where the LDE's DSO explorer sends its DSO โ DMN handoff link. 3002 is in the
backend's local CORS allowlist, together with 3000. So when both frontends run, start
the editor on 3002:
npm appends the arguments after -- to the end of the script string, so this runs
npm run deps:check && vite --port 3002. The appending is confirmed on npm 11.11; Vite's
CLI accepts --port, and a CLI option overrides the config file. Starting the editor
this way has not been tested end to end.
Start the LDE backend¶
The backend needs three things before it starts: Docker containers, a .env, and an
install.
1. The Docker containers. The backend's dev script runs
bash scripts/check-docker.sh first. That script exits 1 unless the ronl-postgres and
ronl-operaton containers are both running and, where they define a health check,
healthy. Both come from the ronl-business-api docker-compose.yml:
| Container | Host port | Notes |
|---|---|---|
ronl-postgres |
5432 | PostgreSQL 16 |
ronl-operaton |
8081 โ 8080 | Operaton 2.1.5 on an H2 file database. Its health check allows a 60-second start period. |
Starting operaton also runs the one-shot operaton-init service, which fixes
ownership of the volume first. Once the containers exist, docker start ronl-postgres
ronl-operaton is enough. See RONL Business API โ Local Development
for that stack.
The LDE backend stores assets in a lde_assets database in the same PostgreSQL. Its
.env.example gives the one-time setup:
docker exec -it ronl-postgres psql -U postgres -c "CREATE USER lde_user WITH PASSWORD 'lde_password';"
docker exec -it ronl-postgres psql -U postgres -c "CREATE DATABASE lde_assets OWNER lde_user;"
docker exec -it ronl-postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE lde_assets TO lde_user;"
2. The .env.
git clone https://github.com/sgort/linked-data-explorer.git
cd linked-data-explorer
cp packages/backend/.env.example packages/backend/.env
Without it, OPERATON_BASE_URL and TRIPLYDB_ENDPOINT are empty, and
packages/backend/src/utils/config.ts throws
Missing required configuration: triplydb.endpoint, operaton.baseUrl when the backend
loads. The example file's values are the local ones: port 3001, Operaton on
localhost:8081, and a CORS allowlist that includes http://localhost:3000 and
http://localhost:3002.
3. Install and start.
The backend listens on http://localhost:3001.
LDE root scripts¶
| Script | Starts |
|---|---|
npm run dev |
The frontend only, on port 3000 |
npm run dev:backend |
The backend only, on port 3001 |
npm run dev:full |
Both, through concurrently |
Each runs the LDE's own deps:check first. There is no dev:frontend script. See
Linked Data Explorer โ Local Development
for the explorer itself.
Environment variables¶
Only variables prefixed VITE_ reach the browser. The REACT_APP_ names from before the
v2026.09.1 Vite migration are no longer read. Which tracked file applies depends on the
Vite mode:
| File | Loaded by |
|---|---|
.env.development |
npm start (Vite dev server, development mode) |
.env.production |
npm run build, locally and in both deploy workflows (production mode) |
.env.acceptance |
Nothing. No script passes --mode acceptance. The ACC workflow builds in production mode and overrides VITE_BACKEND_URL with the acceptance backend. |
.env.example |
Nothing โ a template listing the variables. |
For a personal override, Vite also reads .env.development.local; .gitignore excludes
it and the other .local variants.
| Variable | Purpose | .env.development |
Fallback when unset |
|---|---|---|---|
VITE_BACKEND_URL |
The LDE backend โ SPARQL proxy, SHACL validation, TriplyDB service update, DSO handoff, DMN validate/deploy/evaluate | http://localhost:3001 |
http://localhost:3001 in DMNTab.jsx, useDsoImport.js, shaclHelper.js and triplydbHelper.js; https://acc.backend.linkeddata.open-regels.nl in ronlHelper.js |
VITE_OPERATON_URL |
The Operaton base the DMN tab shows as its Base URL, and from which it builds the evaluate URL it publishes as cprmv:implementedBy |
http://localhost:8081 |
https://operaton.open-regels.nl |
VITE_ENV |
Set in every .env file, read nowhere in src/ |
development |
โ |
VITE_BUILD_SHA, VITE_BUILD_RUN |
Build provenance, injected by the deploy workflows โ see Build Provenance | โ | local build |
The fallbacks only matter when a mode's file does not set the variable; .env.development
sets both URLs, so a normal npm start uses localhost throughout.
VITE_OPERATON_URL decides what a published DMN claims, not where it deploys
Deploy and evaluate both go through the Linked Data Explorer backend, which
targets its own configured Operaton โ so where a DMN lands in local
development is decided by the local backend's OPERATON_BASE_URL, not by this
variable. What this variable does decide is the evaluate endpoint the editor
records for the decision and writes into the TTL as cprmv:implementedBy.
Publishing from a local session therefore publishes a localhost:8081
endpoint. Publish from acceptance or production, whose builds name the shared
engine.
Build and preview locally¶
npm run build runs in production mode, so it reads .env.production: a local build
points at the production backend, https://backend.linkeddata.open-regels.nl. The
deploy workflows get their acceptance or production backend by setting
VITE_BACKEND_URL on the build step, not from .env.acceptance.
npm run preview uses the same fixed port as the dev server:
preview: { port: 3000, strictPort: true }.
Tests and hooks¶
See Testing for the per-layer scripts, the Playwright journeys and the current test count.
Husky installs two hooks:
| Hook | Runs |
|---|---|
pre-commit |
npx lint-staged โ prettier --write and eslint --fix on staged files under src/, and prettier --write on package.json |
pre-push |
npm run deps:check, then npm run lint, then npm run check-format |
Neither hook runs the tests. The ACC deploy workflow runs npm run lint and
npm run test:ci before it builds, so a failing test blocks the pull request โ but the
feedback arrives in CI rather than on your machine. Run npm run test:ci yourself before
pushing.
Getting a change to ACC¶
acc accepts pull requests only
The acc supply-chain gate ruleset on acc requires a pull request and three
passing checks, with no bypass actors. A direct push is rejected, for the
repository owner as much as anyone.
| Required check | Workflow |
|---|---|
audit |
zizmor.yml (Supply-chain audit) |
scan |
semgrep.yml (Semgrep) |
Build and deploy ACC |
azure-static-web-apps-orange-beach-0574c2a03.yml (Deploy ACC) |
# 1. Work on a branch, never on acc directly
git checkout -b feature/your-topic
# 2. Commit, push the branch, and open a pull request against acc
git push -u origin feature/your-topic
gh pr create --base acc
# 3. Watch the required checks
gh pr checks
# 4. Merge once green. Merging is the push to acc, and triggers the deploy.
gh pr merge <n> --merge
The repository allows merge commits only โ squash and rebase merging are disabled โ and deletes the head branch on merge.
Preview deployments. The Deploy ACC workflow's changes job lists the pull
request's files. Build and deploy ACC builds and uploads a Static Web Apps preview only
when at least one file falls outside docs/, .claude/ and *.md. For a docs-only pull
request the job is skipped, which reports success, so the required check is still
satisfied. If the changes job itself fails, the build runs anyway.
See Deployment for the pipeline and Supply-Chain Pinning for the gate.
Maintainer scripts¶
These are not part of day-to-day development; several are run at each release.
| Script | What it does |
|---|---|
npm run lint:fix |
eslint . --fix |
npm run format |
prettier --write . |
npm run check-supply-chain |
Checks that each pinned GitHub Actions digest resolves to the version its comment claims, and that SECURITY-PIPELINE.md still matches the workflows. --offline skips the API. |
npm run check-mirror |
Compares acc and main on the gitlab mirror with GitHub. Never pushes; prints the command and exits 1 on drift. |
npm run check-previews |
Lists Azure Static Web Apps preview environments with no open pull request. Needs an Azure login. Never deletes. |
npm run sbom |
Writes the release SBOM (CycloneDX, production dependencies only) to docs/sbom/. --check exits 1 if it is missing or stale. |