# ADR-0018: Diploma upload is stored in the ZGW Documenten API, fronted by the ACL - **Status:** Accepted - **Date:** 2026-07-20 - **Deciders:** Respellion engineering - **Relates to:** S-10b (#103); proposal #107. Builds on ADR-0001 (ACL is the only ZGW caller), ADR-0003 (ACL default-fill), ADR-0017 (document-wait + provision trigger). Carves the zaak-close on expiry to #106 (S-10c). ## Context S-10a wired the "documenten aanleveren" trigger (portal → BFF → domain → complete the WachtOpDocumenten wait) with the file itself stubbed. S-10b makes the upload real: the diploma must be **stored in the ZGW Documenten (DRC) API** and related to the zaak. §8.1 makes the ACL the only code that talks to ZGW. The DRC API is served by the same OpenZaak container as the Zaken/Catalogi APIs. ## Decision **The ACL fronts the Documenten API: it creates an `enkelvoudiginformatieobject` and relates it to the zaak. The file travels base64-encoded in JSON across every hop (the portal encodes it client-side); a "Diploma" `informatieobjecttype` is seeded in the catalogus and injected into the ACL like the zaaktype.** - **ACL gateway.** `OpenZaakGateway.StoreDocumentAsync` POSTs the `enkelvoudiginformatieobject` (`/documenten/api/v1/enkelvoudiginformatieobjecten`, base64 `inhoud`, `bestandsomvang`, `status=definitief`) then relates it to the zaak (`/zaken/api/v1/zaakinformatieobjecten`), reusing the established gateway patterns (ZGW Bearer JWT, buffered non-chunked body for uwsgi, **no CRS headers** — the Documenten API is not geo, unlike zaak-create). `AclService.StoreDiplomaAsync` default-fills the ZGW-mandatory fields (informatieobjecttype, bronorganisatie, vertrouwelijkheidaanduiding, `taal=nld`, creatiedatum); the domain hands over only the zaak, the bytes, and the file's name/type. No new ZGW scopes were needed — the seed applicatie holds `heeft_alle_autorisaties`. - **The file travels as base64 JSON end-to-end.** The portal reads the chosen file client-side (`FileReader`) and posts `{ contentBase64, fileName, contentType }` as JSON to the BFF; the BFF forwards it to the domain, and the domain to the ACL, all as JSON. This deviates from proposal #107's "multipart on the portal→BFF hop": base64 JSON keeps **one** contract shape across all four services (no `IFormFile`/antiforgery plumbing, no multipart in the generated client), and a diploma is a small placeholder PDF, so the ~33% base64 overhead is immaterial. The ACL turns the base64 back into the ZGW `inhoud`. - **Storing precedes completing the wait.** `ProvideDocuments` (from S-10a) now stores the diploma via the ACL — once the zaak is opened — and then completes the `WachtOpDocumenten` task, so a registration reaches beoordeling only after its diploma is stored. Both steps stay best-effort about missing preconditions (no zaak yet → skip storage; no process yet → skip completion), mirroring withdrawal. - **Catalogus.** `seed_catalogus.py` (OZ_PUBLISH) creates a "Diploma" `informatieobjecttype`, relates it to the zaaktype (`zaaktype-informatieobjecttypen`, while both concept), publishes both, and prints `INFORMATIEOBJECTTYPE_URL`; verify-domain injects it as `Acl__Defaults__InformatieobjecttypeUrl` (a zeros-uuid placeholder otherwise, so the ACL still boots). ## Consequences **Positive** - §8.1 stays intact: the ACL is still the only ZGW caller; the portal only talks to the BFF; the domain only crosses the ACL boundary. Adding a document was almost entirely additive (one gateway method, one default, one seed block). - One JSON contract shape across portal/BFF/domain/ACL keeps the generated client and the service contracts uniform; the upload is exercised live (ACL integration test against real OpenZaak; the Playwright journey uploads a real PDF). **Negative / costs** - Base64 inflates the payload ~33% and holds the whole file in memory at each hop — fine for a small diploma, but not a pattern to reuse for large documents without streaming/multipart. - The zaak is **not** set to a cancellation status when the 30-day term lapses — carved to #106 (S-10c), which adds the cancellation statustype/resultaattype + ACL method + expiry-worker wiring. - Providing documents before the zaak is opened silently skips storage (best-effort); the e2e/live flow avoids this by uploading only after the openbaar register shows the zaak (INGEDIEND). ## Alternatives considered - **Multipart on the portal→BFF hop** (proposal #107). Rejected: it splits the transport into two shapes (multipart then JSON), needs `IFormFile` + antiforgery handling and a multipart method in the generated client, for no benefit at diploma size. - **The domain talks to the Documenten API directly.** Rejected outright: violates §8.1 (only the ACL talks to ZGW).