# ADR 0022: Classification as first-class page metadata - Status: proposed - Date: 2026-07-29 ## Context VS-NfD content must be marked, in every output that leaves the system. Dorfteich has no classification concept today: `model Page` carries title, slug, tree position and timestamps, and nothing else that could express a protection level. The obvious shortcut is to reuse labels. Verification shows why that fails: - `Label` is **pond-scoped** (`pondId`), so the same classification would be a different object in every pond, with no instance-wide meaning. - Labels are **user-editable** by any editor; a marking must not be removable as a matter of routine content work. - Labels do **not inherit** down the page tree, so a subpage of classified content would silently be unmarked. - Labels **never leave the application**: `export.service.ts` loads `labelIds` only to feed `permissions.filterPages`, and no export path writes them out. A carrier that does not reach the output channels cannot serve as a marking. The second question is architectural: should the application separate classification _levels_? It must not (ADR 0019, and the plan's Phase 0 guardrails). Separation is a platform property. ## Decision 1. **A dedicated enum field on `Page`**, with an instance-wide default from `instance_settings`. Not labels, for the four reasons above. 2. **Separation of levels happens outside the application: one instance per classification level.** The application marks; it does not isolate. This is the central operational decision of the whole undertaking and belongs here rather than in a manual, because it defines what the feature is _not_. 3. **The application-side ACL is order, not a protection mechanism.** Permissions keep working as they do (central, default-closed, deny-wins), and the classification field does not change them. Anyone reading the code must not mistake the field for an isolation boundary — the test in #204 pins that. 4. **Classification inherits down the page tree.** A new or moved page takes at least its parent's level. Raising is ordinary editorial work; **lowering requires a dedicated capability** in the central permission model and is audited with old value, new value, actor and page. 5. **Every output channel carries the marking**, and each is an independently closable issue: web view, browser print, server-side PDF, DOCX/ODT, Markdown ZIP, feeds, public API, search results, no-JS shell, attachment download. A channel that cannot carry it internally (arbitrary binary attachments) is marked externally — filename prefix plus companion file — and the remaining gap is a documented residual risk, not a silent one. 6. **Unclassified content shows no marking.** Marking everything trains users to ignore markings. 7. **The marking wording is fixed and locale-independent:** `VS – NUR FÜR DEN DIENSTGEBRAUCH` — the official formula of the German VSA. It is deliberately **not** translated: a marking is a fixed legal formula, and a localized variant would not be the marking. Only the UI labels _around_ it (e.g. an accessibility label naming the element) are i18n'd. The single source is `classificationMarking()` in `@dorfteich/shared` (`packages/shared/src/pages.ts`); no output channel hard-codes the string. ## Consequences - Ten issues, because there are ten output paths; that is the honest cost of "in every output". - The no-JS shell and the SPA are separate render paths, so each needs its own assertion. Likewise the TipTap NodeView path and the server-side `docToHtml` path differ structurally. - The field is a precondition for the read-access audit trail (ADR 0023), which is scoped to classified content only. - Attachments inherit their page's classification. The case where the page link is not yet set (paste-then-insert) fails closed. - Because levels are separated by instance, a page can never "move between levels" inside one deployment — export/import across instances is the path, and its marking is covered by the export channels. ## Implementing issues #204 (field + default), #205 (inheritance + downgrade right), #206 (web), #207 (print), #208 (PDF), #209 (DOCX/ODT), #210 (Markdown ZIP), #211 (feeds/API/search/no-JS), #212 (attachments), #213 (upload warning).