# ADR-0020: The local stack self-seeds the zaaktype, DMN, and NRC abonnement at bring-up - **Status:** Accepted - **Date:** 2026-07-22 - **Deciders:** Respellion engineering - **Relates to:** S-B04 (#110). Local-stack twin of the seeding the verify-* scripts do for CI (`infra/run-domain-check.sh`, `infra/verify-notification-driver.py`). Superseded in part by S-27 (#113), which would let the ACL resolve its zaaktype by identificatie and remove the URL injection. ## Context `infra/docker-compose.local.yml` is the host-browser-friendly stack (`make local`) — the one a developer clicks through the portals with. It had drifted behind three slices, so a fresh bring-up could not complete the flow: 1. The ACL pointed at a placeholder zaaktype (`…/00000000-…`), so zaak creation failed with OpenZaak `400` and the registratie process stuck at `OpenZaakAanmaken` (S-05). 2. `flowable-init` deployed only `registratie.bpmn`, not `diploma-eligibility.dmn`, so completing `WachtOpDocumenten` 404'd on the missing decision and never reached `Beoordelen` (S-10a/S-13). 3. No NRC abonnement was registered, so notifications reached NRC and went nowhere — the projection and the openbaar register stayed empty (S-06). The CI stack (`infra/docker-compose.yml`) does not hit this because its `verify-*` scripts seed the zaaktype, deploy the DMN, and register the abonnement at *test* time. The local stack has no such harness — a developer just runs `make local` and browses. The non-obvious wrinkle is (1): the zaaktype **UUID is assigned by OpenZaak at creation**, so the ACL's zaaktype URL is not knowable when the compose file is written and cannot be a static value. ## Decision **Make the local stack self-seed at bring-up via one-shot init containers, and hand the ACL its server-assigned zaaktype URL through a shared-volume env file it sources on startup.** - **DMN (gap 2).** `flowable-init` now deploys `diploma-eligibility.dmn` to the DMN engine (`/flowable-rest/dmn-api/dmn-repository/deployments`) as a separate deployment alongside the BPMN — identical to the CI `flowable-init`. Idempotent. - **Zaaktype + ACL wiring (gap 1).** A `local-seed` one-shot runs the existing `infra/openzaak/seed_catalogus.py` (`OZ_PUBLISH=1`) against OpenZaak and writes the resulting `Acl__Defaults__ZaaktypeUrl` / `…InformatieobjecttypeUrl` / `Acl__OpenZaak__BaseUrl` into `seed-env:/out/acl.env`. The ACL mounts that volume read-only and overrides its entrypoint to `sh -c 'set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll'`, so the real values override the compose placeholders before the app reads config. The ACL `depends_on: local-seed (service_completed_successfully)`. - **Abonnement (gap 3).** A `nrc-subscribe` one-shot registers an abonnement on the `zaken` kanaal pointing at the event-subscriber's `/notifications` callback (`infra/local/register-abonnement.py`). It is a leaf — nothing depends on it — so it can wait for the event-subscriber without forming a cycle with the ACL bootstrap. - **Reach OpenZaak/NRC by container IP, not service name.** Both the seed's ZTC calls and the abonnement's `callbackUrl` are validated by Django's URLValidator, which rejects a single-label host like `openzaak` / `event-subscriber`. The scripts resolve the target's container IP at runtime (as `infra/run-domain-check.sh` does), keeping the seeded URLs valid **and** host-consistent — the ACL's base URL is set to the same OpenZaak IP that owns the zaaktype URL. - **Acceptance.** `make verify-local` (`infra/run-local-flow-check.sh`) submits against a fresh stack and asserts the zaak opens, the case reaches the werkbak after documents, and the reference appears in the openbaar register — the red-to-green test for all three gaps. ## Consequences **Positive** - A fresh `make local` completes the full demo (submit → werkbak → openbaar) with no manual seeding — the slice's stated outcome. - Reuses the proven CI mechanisms (`seed_catalogus.py`, the DMN deploy, the abonnement driver) rather than inventing new ones; the only genuinely new piece is the entrypoint-sourced env file. - No service code changes — the fix is entirely in `infra/` (compose + two small scripts), so the ACL image and the CI stack are untouched. **Negative / costs** - The two compose files diverge further: the CI stack seeds at test time, the local stack at bring-up. Mitigated by reusing the same underlying scripts and cross-referencing them. - The ACL entrypoint override couples the local ACL to the seed-written file path (`/seed/acl.env`); if the seed fails, the ACL fails to start (loud, healthcheck-visible — preferred over silently running with a placeholder). - Container-IP-based URLs are re-derived on each bring-up; a keep-volumes restart with a changed OpenZaak IP relies on OpenZaak rebuilding hyperlinked URLs from the request host (it does) so the idempotent re-seed reports current-IP URLs. ## Alternatives considered - **ACL resolves its zaaktype by identificatie (`BIG-REGISTRATIE`) at startup.** The cleaner, less-brittle design — no server-assigned URL to capture — and it would help the CI stack too. But it changes a service's runtime behaviour and its config contract, needs new ACL tests + mutation coverage, and still needs a seed step to *create* the zaaktype. Deliberately split out as its own slice with its own ADR (S-27 / #113) rather than folded into this infra-only fix. - **A documented `make local-seed` step run after `make local`.** Smallest change, but it fails the slice's "no manual seeding" outcome — the local stack is exactly the one meant to just work in a browser. Rejected. - **Fixed zaaktype UUID via OpenZaak `setup_configuration`/fixtures.** OpenZaak assigns UUIDs on POST; declaratively creating a fully *published* zaaktype (statustypen + resultaattypen validated against the Selectielijst + roltypen + iot relations) is not something `setup_configuration` supports cleanly in 1.28.2. Rejected as more fragile than reusing `seed_catalogus.py`.