Skip to content

Architecture Decision Records ​

Architecture Decision Records (ADRs) document the significant architectural choices made across our projects. Each record captures the context, options considered, the decision, and its consequences.

Why We Write These ​

When you join a project or review a pull request, you often ask "why is it done this way?" These records answer that question. They capture not just what was decided, but why — including what we considered and rejected.

How to Read an ADR ​

Each ADR follows the same structure:

  • Status — Proposed (under discussion), Accepted (implemented or being implemented), Superseded (replaced by a newer decision)
  • Context — The problem or situation that prompted the decision
  • Options Considered — Alternatives we evaluated, with reasons for rejection
  • Decision — What we chose and how it works
  • Consequences — The trade-offs we accepted, both positive and negative

Current Status of Every Decision ​

The table below indexes the decisions themselves. For where each one actually stands — which territories have adopted it, what shipped, what is still in flight, and what each amendment changed — see the ADR Status Index. It covers all 52 decisions including the eight that are war-room-internal and have no page here.

Read the status index before acting on any ADR's current state: a decision page tells you what was decided, not how far it has travelled.

Decision Index ​

Kendo ​

#DecisionStatusSummary
1Audit Logging SystemAcceptedPer-entity audit tables with hash chains for ISO 27001 compliance
6Two-Tier AuthorizationAcceptedRoute-level model permissions + action-level interaction permissions
3AI Interaction LoggingAcceptedThree-channel logging for outbound AI calls, MCP tools, and AI runs
8Multi-TenancyAcceptedDIY database-per-tenant for 3 fixed companies
13Adapter-Store PatternAcceptedCustom reactive store factory over Pinia for 15+ domain stores
25GitHub Integration SplitAcceptedSplit GithubService into OAuth handshake + bearer-token-parametrized API client; per-call token argument

Brick Inventory ​

#DecisionStatusSummary
4Import AtomicityAcceptedSave-what-you-can with honest reporting for API imports

Emmie ​

#DecisionStatusSummary
22Schedule End Date InclusiveAcceptedSchedule.end_date is the last active day; canonical predicates use inclusive <= / >=
23Schedule Mutation ChokepointAcceptedSingle Action owns Schedule date mutation; wrapping normalizer, schema unchanged
31Datetime Instant-vs-Wall-Clock ClassificationAcceptedEvery datetime field declared instant or wall-clock; wall-clock serialized naive (self-describing wire); backend WallClock* casts + arch test, frontend Temporal helper (polyfilled)

Town-crier ​

#DecisionStatusSummary
36Cross-Harness Producer IngressAcceptedOne bus-hosted GitHub App replaces 14 per-repo announce-pr.yml; HMAC-verified pull_request/pull_request_review ingress dispatching in-process to announce/resolve/consensus with the internally-minted github-action identity; first bus outbound HTTP (timeout-bound, installation_id-keyed token cache); pull-first /github/health observability for the silent per-org failure mode

Cross-Project ​

#DecisionStatusSummary
2Cascade Deletion & Soft DeletesAcceptedModel declares, Action executes, tests verify. No ON DELETE CASCADE.
9Unified ResourceData PatternAccepted (amended 2026-07-03)Custom ResourceData base class replaces Laravel's JsonResource; loud-failure eager-load gate + Hydrator supply side
11Action Class ArchitectureAcceptedfinal readonly Actions with single execute() method, explicit DI
12FormRequest → DTO FlowAcceptedType-safe pipeline from HTTP validation to business logic
14Domain-Driven Frontend StructureAcceptedVertical slices by business domain, not technical layers
16Config Attribute InjectionAccepted#[Config] attribute for all config access, config() helper prohibited
17Page Integration TestsAcceptedMount domain pages with real components, mocked services, separate coverage accounting
19Explicit Model HydrationAcceptedBan $fillable/$guarded, require explicit property assignment in Actions
15ADR GovernanceAcceptedSingle source of truth at adrs.script.nl
18ISMS Information SystemAcceptedLaravel + Vue ISMS with policies as markdown, operational data in PostgreSQL
20Input/Result DTO SplitAcceptedSplit DTOs by usage direction at the Action boundary — arch tests enforce
21PHPStan Rules PackageAcceptedCanonical war-room PHPStan rules distributed as script-development/phpstan-warroom-rules Composer package
24Automated External ProvisioningAccepted (amended 2026-05-20)Async + provider-abstracted + rollback-on-failure + audit-mandatory + flag-gated for tenant-resource provisioning
26Decrement Action SymmetryAcceptedCaller-side symmetry discipline + Pest arch test for counter-mutation decrement Actions; no internal floor guard at Action layer
27Canonical Territory SkeletonAcceptedBIO-aligned baseline + Audit/ for app/; Architecture/ for tests; apps/<role>/ + shared/ for frontend; trip-point arch tests from day 1
28Canonical Git Hooks (v1)AcceptedPlain core.hooksPath + .githooks/ shell scripts; fast pre-commit + heavy pre-push; per-territory copy from /templates/githooks/; lefthook + distributed package as documented future iterations
29Audit Row Durability ContractAcceptedAudited Actions' DB::transaction(...) closure contains only DB writes; throws exit via sentinel-return; non-transactional state mutates post-commit
32Cached-Store Hash-Bumping ProtocolAcceptedSubscribe-via-header: SPA declares cached keys per request; middleware authorizes (per-key Tier-1 policy) + stamps the entitled subset, one-resolve-per-distinct-parent (N+1 arch-tested)
33AVG Erasure by Anonymization-in-PlaceAcceptedErasure = anonymize-in-place (not hard-delete): per-column scrub contract across the user aggregate + framework tables, S3 delete, emmie revoke-only + OpenAI persist-then-delete residuals, audit-trail identity-PII accepted as lawful record; completeness Feature+arch tests
34Ambulatory Financing Care-Type / Rate-Interval CouplingAcceptedEmmie territory-specific: ambulatory registration ↔ AMBULATORY financing ↔ HOUR/MINUTE interval three-way coupling; interval-keyed Invariant A (revenue correctness) enforced at the registration chokepoint + arch test; care-type-keyed Invariant B (hygiene) held for prod breach-check
35Appointment-Slot Inverted-Window DB CHECK BackstopAcceptedwijs territory-specific: end_date >= start_date enforced by a DB CHECK on appointment_slots (MySQL 8.0.45) as a write-path-agnostic Level-1 backstop below the shipped Action/validation guards; two-step clean-then-constrain (repoint 9 cancelled-appointment occupations → delete 576 inverted rows → ADD CHECK)
37fs-http Middleware Guarding by DefaultAccepted (shipped 0.6.0 2026-07-11)fs-http register*Middleware auto-wrap the body in guarded() (loud-swallow) by default; per-call {guard:false} opt-out + service-level onMiddlewareError; supersedes the "every consumer MUST guard" obligation of Principle #8. Loud-swallow is the third option unavailable in 2026-05-13 (serves "be loud" without corrupting the request); WR-0290 proved opt-in doesn't hold. Consumer obligation stands until the fs-http minor ships
38Daymate Data-Encryption & Privacy DoctrineProposedDaymate territory-specific: three-axis frame (confidentiality→encryption / read-authz→consent / integrity→tamper-evidence); server per-column app-level encryption scoped explicitly to DB-only compromise (key co-resident with DB_PASSWORD, no KMS — does NOT cover app-compromise/secrets-bundle); scope drawn on free-text-vs-structured axis + widened; mood-plaintext needs a named future aggregation; email blind-index escape hatch; framework-table (jobs/sessions) caveat. Pending expert review + 3 operational confirmations
39Cross-Harness Merge GateAcceptedTown-crier cross-harness contract: review findings become review_findings rows (enum tier/confidence/provenance); merge-gate state derives on the model — BLOCKED (Confirmed Blocker) / HELD (Confirmed Major or Unconfirmed Blocker) / CLEAR — never stored, binds bus machinery only (suppresses consensus auto-resolve, never forces a verdict — R1); findings persist across heads until dispositioned (fixed/follow_up/declined/refuted) through one FindingDisposer chokepoint with a no-self-verification guard (R4); the App emits a town-crier/gate check run per head (per-repo opt-in requiredness, fail-open, lock-anchored atomic emit — the teeth). Built + prod-live (TC-0041, #133/#134/#135, Fly v44); gate_mode (TC-0054) pending
40Cross-Harness Review Thread & Turn-Lock ProtocolAcceptedTown-crier coordination substrate: reviews serialize into a THREAD (announce-once keyed on pr_url → transient turn-lock claim returning prior reviews → submit a stance {confirm/deny/independent} releasing the lock → next harness); no-two-turns-in-a-row (decorrelation); ends via any-harness resolve or hard cap (TOWN_CRIER_REVIEW_CAP=5); stale lock (>TOWN_CRIER_LOCK_TTL_MIN=15m) stealable (no deadlock); turn-lock = single conditional UPDATE (TOCTOU-free), submit re-checks holder under lockForUpdate(); identity server-stamped never input; roster invite-based (unique slug on join, no squat); reviews[] full history vs per-round review_count. ClaimRaceTest pins 5 invariants. Prod-live 4-way
41Cross-Harness Severity LadderAcceptedTown-crier shared review vocabulary (TC-0036): 🛑 Blocker / 🔴 Major / 🟡 Minor / ⚪ Nit first-match tree (ship/edge/objective-quality/taste tests) + Praise outside it; one ladder all categories (severity=consequence not type); as-if-true (uncertainty → confidence tag, never a lower tier); tags = confidence (Confirmed/Unconfirmed — a 2nd harness turn promotes/kills) + provenance (introduced/adjacent/pre-existing, strict — only introduced blocks); tie-break round-down except security/data-integrity/acceptance-criterion/regression round-up (R3); riders R1 (verdict-binding, superseded by ADR-0039 gate) / R2 (invisible criteria N/A) / R4 (approve-with-residuals → disposition). Enum-backed on the wire (TC-0040 → ADR-0039). Ratified + skills migrated
42Per-Repo Read Authorization, Derived from GitHub AccessAcceptedTown-crier cross-harness contract: retires D1 ("any bus member reads all") for a per-repo read scope resolved from the member's GitHub login via the App's installation credentials, cached off the hot path, fail-closed; scopes index/show/batch + the write verbs and folds in the enact-verb authz question. Accepted 2026-10-02 at /muster; live since town-crier #214 (2026-07-17).
43Headless + Themeable Shared Vue UI ComponentsAcceptedCross-project: shared Vue input components as a HEADLESS behavioural core (select/combobox/field — the 4×-reinvented controls) + a STRUCTURAL CSS-var theme contract (34 --ui-* props: palette AND border-width/radius/shadow-shape/label-transform — a colours-only contract fails, spike-proven kendo→BIO) + error-as-prop (never imports a territory error service); lives as a new ui-* family in the fs-packages monorepo (NOT fs- — that's frontend service), pkg @script-development/ui-inputs, peers on fs-theme; graduate to its own repo if it grows. 0.2.0 published; BIO pilot (WR-0431 / PR #284) validated the token + error-prop contract end-to-end → Accepted 2026-07-16. Follow-ups: Number/Date/Textarea atoms + candidate bg-state vars = WR-0460, combobox AT-focus = WR-0441. Phase-1 UnoCSS-core; date pickers deferred. kendo=donor, BIO=first live consumer
44Collection Response Shape — Bare Arrays vs Cursor PaginationAcceptedCross-project: list-response convention. BOUNDED sets (known ceiling) return a BARE JSON array + client-side pagination (kendo/emmie withoutWrapping, isms array-backed Data); UNBOUNDED DB-backed registers return a Laravel CursorPaginator envelope ({data,next_cursor,prev_cursor,per_page}) — cursor MANDATED over offset (stable under inserts, BIO precedent, per_page cap 100). Tier is a design-time per-endpoint call keyed on unbounded-growth potential; the two shapes coexist and are never retrofitted. Accepted trade-off: a bounded list that unexpectedly outgrows FE pagination takes a one-time breaking bump
45The War Tent — Commander's Field Command PostAcceptedWar-room territory-specific: found war-tent (Goosterhof/war-tent, private; submodule territories/war-tent) — a Windows-native Tauri v2 + Vue 3 desktop command post bridging into WSL2. Substrate inherited from the lab's Mezzanine (AD-1 pty-wrap of claude via wsl.exe, POSIX path hygiene, Ascent updater pattern), soul not (own metaphor/voice/semantics). Rulings: name war-tent (war-table collides with a lab codename, command center too generic); SIX FIXED seats for Generals (count is config — Workbench precedent distinguished: all Generals target one war-room root); spy Score: N/10 memory stamps forward (all 8 SOPs amended — territories tab averages as stamps accumulate, tolerates absence); design = a named lab campaign (Artisan/Illusionist) before Phase 1 visuals; updater keys Commander-gated. Phase 0 landed 2026-07-16
48Error Feedback Must Reach the UserAcceptedCross-project silent-failure contract: every failed request MUST produce user-visible feedback at the moment of action. Terminal fallback claiming every status the specific handlers don't (incl. 5xx/network/timeout — the wijs hole); a submit path that awaits MUST catch (declared, never a bare finally); error-bag checks test NON-EMPTY own-keyed object at ONE shared predicate ({} is truthy — entreezuil #301 silenced three surfaces); no status special-cased into an empty body and no frontend replacing a server message by status alone (emmie blanks a 500 twice); a new error SHAPE is a two-layer contract change shipped with its FE render path. Level-1 teeth-proved fallback spec + bag regression matrix per territory; bag predicate → Armory. n=4 incidents / 3 territories (wijs WR-0425 + WR-0956 — the latter reached a paying customer, entreezuil #301, emmie 500-blanking)
50fs-auth — the Armory's Sanctum SPA Session PackageAcceptedCross-project: one Armory package owns the Sanctum SPA-cookie SESSION — createSessionStore({guard, http, endpoints, parseUser, csrf?}), explicit endpoint map, four-state machine, outcome-returning login/logout (ADR-0048), single-flight onSessionEnd, hardened resolveSafeRedirect, guard registrars on fs-router/fs-http. 401 + 419 = one class, one action; 419 is dev-only on a same-origin SPA (Laravel 13 hasValidOrigin). Package never navigates and never renders copy. Stays territory-side: permissions, 2FA, OAuth, inactivity, sentences. Adoption: lokalekeuze (WR-1383 becomes the adoption) → kendo (ally Jasper first) → forks → skeleton. Accepted 2026-10-22; 0.3.2 on npm, lokalekeuze on it in all four apps. Seed: kendo deep dive 2026-09-14 + fleet survey 2026-09-12; WR-1384.
51Enums on Database Columns Are Int-BackedAcceptedCross-project: a backed enum cast onto a DB column is INT-backed (emmie/kendo convention); a string enum on a column is a ruling — survivors today are an external system's vocabulary (SnelStart) and the hash-chained audit actor, on utf8mb4_0900_bin (never utf8mb4_bin, PAD SPACE). API emits the int, frontend mirrors a numeric map; audit details carry a frozen per-case declared word (the word stored before conversion), never the code; conversions are LOCKED and re-runnable (an unlocked one wedges releases); Laravel binds only string-backed route enums. First adoption lokalekeuze (LK-0967, 5 slices 2026-09-27), then laravel-skeleton.

Architecture documentation for contributors and collaborators.