Headless Drupal 11 backend for Danish municipal workflow automation.
Pre-pilot POC. Live at https://api.aabenforms.dk with a Nuxt frontend at https://aabenforms.dk. MitID runs against a Keycloak mock; Serviceplatformen (CPR/CVR) and Digital Post run against test or mock endpoints, not live production integrations. For a claim-by-claim account of what works today versus what is demo or planned, see docs/PROMISES-VS-VERIFIED.md.
A modular platform for Danish kommuner to automate citizen-facing workflows and connect to government services (MitID/NemLog-in, Serviceplatformen, Digital Post). Workflows are built in a visual editor and run as event-driven (ECA) flows.
Real today:
- ECA workflow engine with a visual Workflow Modeler (
drupal/modeler) and execution replay with token inspection - 18 municipal ECA flows (building and parking permits, marriage and association booking, citizen service, FOI, address and phone change, company verification, dual-parent approval, HR onboarding, mileage, MED election, caseworker review)
- MitID OIDC sign-in against a Keycloak mock IdP; CPR (SF1520) and CVR (SF1530) lookup clients
- Custom Danish webform elements with server-side validation: CPR (format and modulus-11), CVR, Adressevælger address autocomplete
- Field-level CPR encryption and audit logging (in
aabenforms_core) - Digital Post (SF1601) in
fake_dbandwiremocktest modes - A persistent case (sag) engine: each case carries a lawful status lifecycle and a deadline (frist) clock, and every transition is an auditable revision. The full loop is wired end to end — open a case, journalise it (SF1470), decide it lawfully (FVL §19 partshøring gate + §25 mandatory klagevejledning), send the decision via Digital Post, handle an appeal (genvurdering), and distribute the finished case to a fagsystem via SF2900 where the business receipt closes it. Two flows drive it: a concern report (underretning, statutory 24-hour clock) and an economic free-place application (fripladstilskud, income fetched from CPR → auto-decision → decision letter → SF2900). Cases surface in a caseworker inbox
Demo or mock, not production:
- Payment, SMS, GIS/zoning, payroll and calendar actions are demo mocks
- Digital Post has no live MeMo/SOAP transport yet; MitID has no live registration; Serviceplatformen needs client certificates before CPR/CVR run live
Nuxt 3 frontend -> Drupal 11 backend (JSON:API, ECA, Webform) -> Danish gov services
(aabenforms.dk) (api.aabenforms.dk) MitID, CPR/CVR (Serviceplatformen), Digital Post
Deployment is orchestrated by the contabo-infrastructure repo; a push to main rebuilds the Docker image on VPS2.
git clone https://github.com/madsnorgaard/aabenforms.git backend
cd backend
ddev start
ddev launch # admin UI, login admin / adminLocal URLs: site at https://aabenforms.ddev.site, JSON:API at /jsonapi, mail at :8026. ddev start also boots Keycloak (mock MitID) and WireMock (mock Serviceplatformen) for zero-config local development. The mock realm, test users, and the Keycloak re-import gotcha are documented in docs/DDEV_MOCK_SERVICES_GUIDE.md.
| Module | Status | What it does |
|---|---|---|
aabenforms_core |
Active | Base services: Serviceplatformen client (SF1520/SF1530), AES-256 field encryption, audit logging, tenant resolver, workflow execution collector |
aabenforms_workflows |
Active | ECA action plugins, the approval system, and the template wizard. Some actions (payment, SMS, GIS, payroll, calendar) are demo mocks |
aabenforms_mitid |
Active | MitID OIDC sign-in, session management, CPR claim extraction |
aabenforms_webform |
Active | Custom webform elements: CPR, CVR, Adressevælger address |
aabenforms_tenant |
Active | Domain-based multi-tenancy (logical, single database) |
aabenforms_case |
Active | Persistent case (sag) entity with a status lifecycle, deadline (frist) clock and audited transition revisions; ECA actions to open, journalise (SF1470 demo), decide (FVL §19/§25), appeal and transition cases; ships the underretning, fripladstilskud and klage flows |
aabenforms_kombit |
Active | KOMBIT connectors. SF2900 Fordelingskomponent distribution (demo): hands a decided case to a fagsystem and closes it on the business receipt; real SOAP/STS/OCES3/SFTP behind the same contract later |
aabenforms_digital_post (+ _eca) |
Partial | SF1601 Digital Post in fake_db/wiremock; real MeMo and SOAP transport are planned (issue #77) |
aabenforms_nemlogin, aabenforms_sbsys, aabenforms_get_organized |
Planned | NemLog-in Erhverv and ESDH/case-system integrations (issues #79, #84-#86) |
Encryption and GDPR audit logging are built into aabenforms_core. CPR numbers are encrypted at rest (AES-256) on submission and decrypted only at the point of use (registry lookup, Digital Post recipient, audit hashing). The encryption key is read from the AABENFORMS_CPR_KEY environment variable (base64 of 32 random bytes, generated with openssl rand -base64 32); the key and encryption profile are provisioned automatically by a database update and never stored in git. If the variable is unset, submissions still succeed but CPR is stored unencrypted and a warning is logged. A retention and right-to-erasure subsystem does not exist yet and is tracked in issue #91.
A security pass in June 2026 fixed issues found by a local pressure test, where some steps reported success while the underlying control did nothing:
- #68 parent-CPR consent gate: fail-closed, real caseworker CPR fields, submission-scoped session, re-verified at submit
- #69 audit integrity: token resolution so CPR/CVR lookups actually run; MitID validation fails closed; honest step statuses
- #70 execution-replay PII: least-privilege plus a cron self-heal so an armed replay cannot record citizen CPR to a shared store
- #71 docs/PROMISES-VS-VERIFIED.md: the claim-vs-verified table
Also in June 2026 the case (sag) engine landed (aabenforms_case): a revisionable case entity with a lawful status lifecycle (modtaget → oplyst → partshøring → afgørelse → påklaget → lukket), a configurable per-area deadline (frist) clock, and ECA actions that open a case from a submission and advance it through audited transitions. Two flows run on it — underretning (statutory 24-hour clock) and fripladstilskud (income-gated auto-decision: at/under the threshold the case auto-advances to a decision, above it it waits for manual handling) — and a caseworker inbox lists cases at /admin/aabenforms/cases (backend) and /cases/inbox (frontend).
The casework loop was then closed: SF1470 journaling, a lawful decision step (FVL §19 partshøring gate + §25 mandatory klagevejledning with a computed klagefrist), the decision letter via Digital Post, an appeal/genvurdering flow (the paaklaget state, lodged via a klage form), and aabenforms_kombit's SF2900 distribution where the receiving fagsystem's business receipt closes the case. A low-income fripladstilskud now runs the whole lifecycle modtaget → journaliseret → oplyst → afgørelse → lukket as audited revisions. All external integrations are demo behind production-shaped contracts (no live certs/SOAP/SFTP).
The backlog is in GitHub issues, grouped by label:
- Security (
security): circuit breaker #72, Digital Post idempotency #73, replay redaction #74, permission registration #75, consent-gate test #54 - Productionise integrations (
integration): Serviceplatformen certs #76, live Digital Post #77, Beskedfordeler #78, MitID production #79, Adressevælger + Datafordeler #80 - Replace mocks (
mocks): payment #81, SMS #82, GIS/BBR #83 - Case-system / ESDH handoff (
esdh): handoff module #84, SF1470 journaling #85, ESDH adapters #86 - Advanced flows (
flows): SLA and escalation #87, task inbox and gateways #88, appeals and letters #89, persistent history and templates #90 - Compliance (
compliance): GDPR retention/erasure #91, WCAG and NIS2/DPA #92
ddev drush cr # clear cache
ddev drush config:export -y # export config
ddev drush config:import -y # import config
ddev exec phpunit -c web/core web/modules/custom/aabenforms_workflows/tests/src/UnitWorkflow admin is at /admin/config/workflow/eca; the template wizard is at /admin/aabenforms/workflow-templates. See docs/WORKFLOW_GUIDE.md for building flows, docs/DATA-FLOW.md for how data moves through every flow and where the PII sinks and trust boundaries are, and docs/ for the template reference and admin guides.
- Frontend: aabenforms-frontend (Nuxt 3)
- Deployment: contabo-infrastructure
GPL-2.0-or-later. See LICENSE.txt.