42 KiB
Lumen — Architecture Decision Records (Phase 1)
All decisions below follow the evaluation discipline required for this phase: problem → alternatives → criteria → evaluation → decision → reasoning → consequences → risks → reversibility. Decisions were made against Lumen's actual requirements (300–500 attendees, offline-first PWA, small team), not technology popularity.
Status values: Accepted (adopted now), Proposed (adopted pending the listed validation spike).
ADR-001: Offline-first architecture
- Status: Accepted
- Context: The festival environment may have no usable connectivity of any kind (DISCOVERY §3). The product's core promise: everything attendees need works with zero internet.
- Problem: Choose the fundamental data-flow posture: online-first with offline fallback, offline-first with online enhancement, or offline-only.
- Decision: Offline-first. Local storage is the source of truth for all critical functionality. Network connectivity is an enhancement used only for updates/optional live content. No critical code path may perform a blocking network call.
- Alternatives considered:
- Online-first + graceful degradation — rejected: violates invariants 1–4; degradation at a festival is the common case, not the edge case.
- Offline-only (never touches network) — rejected: forfeits schedule corrections, emergency contact updates, and app update delivery; the brief explicitly allows optional online features.
- Evaluation criteria: invariant compliance (1–10); failure-mode simplicity; ops burden; honesty of user-facing state.
- Reasoning: Offline-first is the only posture in which connectivity loss is a non-event. It forces the valuable side-effects this product needs anyway: versioned data packages, verifiable readiness, atomic updates.
- Consequences: one-time online bootstrap required (C-17); explicit readiness state machine needed (ADR-006/§12); all content must be data (invariant 13); a static publishing pipeline suffices (no backend).
- Risks: bootstrap distribution is a product/ops problem (RK-2); users who never prepare arrive with baseline only.
- Reversibility: Not reversible without rebuilding — but no plausible future requirement makes online-first desirable.
- Related: ADR-002, ADR-004, ADR-005, ADR-006, ADR-010.
ADR-002: PWA strategy
- Status: Accepted — addendum after SPIKE-07 (validation: ARCHITECTURE-VALIDATION.md §2)
- Context: Lumen must not be a native app (brief). Target browsers: iOS Safari, Android Chrome. DISCOVERY §12 established install-first as load-bearing (storage persistence, future push, standalone UX on iOS).
- Problem: Define the delivery/installation model and how much to lean on PWA-specific behaviors.
- Decision: Installed-first PWA on a single HTTPS origin. Web app manifest with standalone display; install coaching is a first-class UX flow (manual steps on iOS; native prompt on Android where available). The app must remain fully functional when not installed, while honestly warning that storage may not persist. No native wrappers (no TWA/App Store packaging).
- Addendum (SPIKE-07): Preparation has explicit levels L0 (shell + floor → NOT_READY/BASELINE_ONLY), L1 Minimum Safe (L0 + emergency + schedule → PARTIAL), L2 Full (L1 + map + info + assets → READY) (
SPIKE-07-BOOTSTRAP.md:31). Fixed download orderemergency → schedule → info → map-base → assets; L1 already yields core festival survival. Prep before install is allowed on iOS tab (same origin, survives until eviction) — coach offers "install first (recommended)" and "get data now" (SPIKE-07-BOOTSTRAP.md:58). Maps to readiness predicate C1–C8 (SPIKE-05-OFFLINE-READY.md:28). No direction change; install-first remains load-bearing but never blocks tab use. - Alternatives considered:
- Tab-only web app, no install push — rejected: iOS 7-day eviction of non-installed storage makes the offline promise unreliable (DISCOVERY PW-1).
- Native wrapper (TWA/wrapped webview) — rejected: store dependency violates brief; adds ops without adding capability we need in V1.
- Native app — out of scope by definition.
- Evaluation criteria: storage persistence prospects; install friction; ops surface; compliance with "no app stores" constraint.
- Reasoning: Installation is the single highest-leverage action available on the web platform for this product: it improves storage persistence, unlocks standalone UX, and is the precondition for any future push. The architecture therefore spends UX effort on install coaching deliberately.
- Consequences: install coach UX required; capability detection must distinguish installed vs tab mode; all features must still work in tab mode (degraded-persistence warning only).
- Risks: users refuse/forget to install (RK-1); iOS EU region variability.
- Reversibility: Install coaching can be relaxed later; the single-origin model is hard to reverse (storage is origin-keyed) — accepted.
- Related: ADR-001, ADR-004, ADR-014.
ADR-003: Frontend technology
-
Status: Accepted (with explicit reconsider trigger)
-
Context: Small/solo team (DISCOVERY A-17); low-end Android in audience (A-13); app surface is modest: 4 destinations, lists, details, map, status. DISCOVERY AD-1 left this open.
-
Problem: Choose the UI technology stack.
-
Decision: Vanilla TypeScript (strict), no UI framework. A small internal observable-store module, a small history-API router, and DOM rendering via structured, escaping render functions. Build with a single-bundle bundler producing hashed immutable assets.
-
Alternatives considered:
Alternative Runtime size Churn/longevity risk Complexity for small team Low-end perf Vanilla TS (chosen) ~0 beyond own code None (no deps) Requires discipline (no framework guardrails) Best Preact ~4 KB Low (stable, mature) Familiar mental model Very good Svelte Small output Medium (compiler-coupling; major-version churn) Good DX Very good React ~40 KB+ runtime Medium Very familiar talent pool Good, not best Vue/Angular/Solid Medium+ Low-Medium Good DX Good -
Evaluation criteria: bundle/JS budget on low-end devices; dependency churn over multi-year festival lifetime; operational risk for a small team; accessibility control; framework-neutrality of offline architecture.
-
Reasoning: The app's UI complexity is low and its lifetime is long (reused across festival editions). Every framework would work; none solves a problem Lumen actually has, while each adds bundle weight and version-churn risk. "Simplest architecture that satisfies requirements" (principle 15) selects vanilla TS. This is explicitly not chosen for ideological reasons; the reconsider trigger below is part of the decision.
-
Consequences: hand-rolled store/router (small, tested); stricter code review on UI correctness; zero framework upgrade burden; smallest possible JS.
-
Risks: hand-rolled UI bugs (RK-7); onboarding friction if contributors expect a framework.
-
Reversibility / reconsider trigger: If during implementation the view layer shows sustained complexity beyond simple store→render (e.g., many interdependent live views), migrate to Preact (closest mental model, smallest migration cost). The layering in ARCHITECTURE-DESIGN §6 keeps domain/data code untouched by such a migration.
-
Related: ADR-002, ADR-008.
ADR-004: Local storage
- Status: Accepted (YELLOW — device-conditional) — validated at design-logic level by SPIKE-01 + SPIKE-02; production readiness conditional on physical-device tests per SPIKE-01 §5 (see ARCHITECTURE-VALIDATION.md §2, §4, §7)
- Context (validation update): SPIKE-01 24-item findings confirm IDB is correct for this workload (spec CONFIRMED for transactions/atomicity/multiple DBs); SPIKE-02 16/16 PASS proves A/B ordering makes single-DB atomicity sufficient. Remaining INFERRED/UNVERIFIED items (iOS jetsam atomicity, quota-near-full writes, Blob read-back on low-end) are isolated and queued for device matrix — they do not contradict the decision.
- Context: DISCOVERY AD-3/TQ-2/TQ-3. Everything offline depends on local storage that browsers may evict.
- Problem: Choose storage technologies and layout for shell, datasets, and user state.
- Decision:
- IndexedDB for datasets (A/B slot databases) and user state, accessed through a thin internal promise wrapper (no external DB library).
- Cache Storage for the app shell only.
- localStorage only for trivial, non-load-bearing flags (guarded).
- Assets stored as Blobs inside the dataset slot DB (one failure domain for rollback).
- Alternatives considered:
- Dexie/idb libraries — rejected for V1: access patterns are few and known; an internal wrapper (~small module) avoids a dependency and keeps IDB transaction discipline audited in one place. Reconsider if wrapper proves error-prone.
- SQLite-in-WASM (wa-sqlite/cr-sqlite) — rejected: dataset scale (≤1k events, ≤50 MB) doesn't need SQL; WASM memory limits on iOS and added complexity fail the simplicity criterion. Reconsider only if query needs explode (DD-10).
- OPFS files — rejected as primary: workable but weaker query/transaction semantics; no advantage at this scale.
- localStorage-only — rejected: size limits (5–10 MB) and synchronous API.
- Evaluation criteria: durability semantics; transactional atomicity (needed by ADR-006); Blob support; iOS reliability evidence; dependency surface; quota behavior.
- Reasoning: IDB is the only browser store that offers transactions + structured data + Blobs + generous quotas on both targets. The thin-wrapper choice follows principle 15. SPIKE-01 validated the design at spec/policy level; the sole remaining platform assumption (IDB atomicity under iOS jetsam kill) is isolated to one UNVERIFIED item queued for device testing in SPIKE-01 §5 — it does not block the decision.
- Consequences: A/B slots are separate IDB databases + a system DB holding the active pointer; rollback/activation is one transaction on the system DB; eviction detection via boot verification (no eviction event exists on the platform).
- Validation addendum (SPIKE-01 P1–P8, SPIKE-02 F-1…F-3 — normative): P1 one short txn per staged file (bytes+progress together); P2 activation single txn on
lumen-system; P3 wrapped IDB + QuotaExceededError keeps active; P4 free-space pre-check 2× package viastorage.estimate(); P5 single record ≤6 MB; P6persist()requested but never relied upon; P7 boot light verification as eviction detection; P8 no dataset state in SW. SPIKE-02 adds: F-1 bytes+progress atomic, F-2 verification record in activation txn, F-3readbackPendingflag, F-4 rollback depth 1 accepted. - Persistence correction (2026-08-31): P1 requires the staging journal to be in each target slot database, alongside
filesandassets.lumen-systemcontains active/readback metadata only; it is not a source of per-file staging progress. This preserves one-database atomicity for file/asset + progress without a cross-database transaction. - Risks: iOS IDB edge bugs (mitigation: wrapper isolation, spikes, re-prep recovery); silent eviction (mitigation: RECOVERY state); residual device risk is YELLOW until D1–D4 matrix passes (ARCHITECTURE-VALIDATION.md §4).
- Reversibility: Storage layout is internal to the data layer; migrating to another store later touches DatasetStore/persistence modules only, not features. Medium reversibility.
- Related: ADR-001, ADR-005, ADR-006.
ADR-005: Festival Data Package
- Status: Accepted — addendum after SPIKE-04/05/06/08 (ARCHITECTURE-VALIDATION.md §2). Schema now normative per SPIKE-04 §2 fragments;
requiredflag, no-expiration rule, emergency sub-versioning and user-data schema split are part of the decision. - Validation addendum: F-1
sections[*].required(default true; readiness gates on required only, SPIKE-04/05); F-2 no expiration for datasets/sections — offline device must never see data "expire" (SPIKE-04:50); F-3 emergency independentemergencySchemaVersion/contentVersion/updatedAtfor floor/dataset compatibility (SPIKE-04:70, SPIKE-06:110); F-4 user-dataschemaVersionseparate from package schema with idempotent boot migrations. Budgets and pipeline gates (5 gates, SPIKE-04:189) enforce per-file ≤6 MB, image caps from SPIKE-03, total ≤40 MB. JSON+hash+Ed25519 remains sufficient at this scale (F-5). - Previous status: Accepted
- Context: DISCOVERY §15, AD-4/AD-10. Content must be versioned data (invariant 13); organizers need an authoring path (AMB-2).
- Problem: Define the package format, structure, authoring source, and publication model.
- Decision: Festival Data Package v1 = a directory of JSON section
documents (
emergency,schedule,map,info,assetsinventory) + binary assets +manifest.json(inventory, SHA-256 hashes, sizes, edition, monotonic packageVersion, schemaVersion, compatibility envelope, festival metadata) +signature.json(Ed25519 over the manifest digest). Authoritative source: a content repository (organizer-edited sheets and blocks) transformed by an automated validate→build→hash→sign→upload pipeline. Distribution: immutable versioned URLs on the static CDN + mutablelatest.jsonpointer. - Alternatives considered:
- MessagePack/CBOR binary sections — rejected: ~30–50% size win on ≤3 MB of JSON is not worth losing human-auditability and simpler tooling.
- SQLite file as the package — rejected: couples data format to a storage engine choice; integrity-per-file becomes coarser; rejected engine (ADR-004).
- Single monolithic JSON file — rejected: prevents per-section partial readiness (PARTIAL states), prioritized download order, and section-level quarantine.
- Dynamic CMS/API backend — rejected: adds server state, auth, and ops for zero V1 benefit (A-17).
- Evaluation criteria: authorability; verifiability/integrity granularity; partial-readiness support; size; tooling simplicity; longevity.
- Reasoning: JSON sections give section-level hashes, prioritized downloads, and human-inspectable content (emergency content must be auditable). The manifest centralizes completeness so the app never guesses (invariant 8/10 of discovery R-D3). Static publishing matches the ops reality of a small team.
- Consequences: pipeline tooling must exist (schema validators, budgets, signer); stable event IDs become contractual; content review happens in the repo (audit trail).
- Risks: organizer content quality/variance (mitigation: intake templates + pipeline validation gates — see OQ-2/OQ-6); JSON verbosity if content grows (budgets guard).
- Reversibility: Format is versioned (
lumen.package/1); a v2 format can coexist via schemaVersion envelope. High reversibility by design. - Related: ADR-004, ADR-006, ADR-013, ADR-014.
ADR-006: Atomic updates and rollback
- Status: Accepted (YELLOW — device-conditional) — state machine proven 16/16 by SPIKE-02; production readiness conditional on IDB atomicity under real kills on iOS (SPIKE-01 §5, SPIKE-02 §6) — see ARCHITECTURE-VALIDATION.md §2, §4, §7
- Validation addendum (SPIKE-02 F-1…F-4 — normative): F-1 bytes+progress in same per-file txn; F-2 verification record (what/when/version) in activation txn; F-3
readbackPendingflag set in activation and cleared after readback; F-4 rollback depth = 1 (new staging wipes inactive slot — accepted trade-off, 3-slot rejected for footprint). Ordering proof: single-DB atomicity suffices because inactive slot is fully written+validated before pointer moves; quarantine + monotonic versions prevent replay of bad packages. - Persistence correction (2026-08-31): The F-1 progress record is the authoritative
stagingjournal inside the target slot database. It is committed in the same short transaction as its corresponding file or asset. The system database retains only the active pointer, verification/readback metadata, and boot metadata; no cross-database transaction is used for staging. - Context: Invariants 5–8; DISCOVERY AD-6; iOS SW-kill reality (PW-6).
- Problem: Apply dataset updates such that the observable state is always "old valid dataset" or "new valid dataset", surviving interruption at any stage; provide rollback.
- Decision: A/B dual-slot model. Two dataset slots (IDB databases).
Updates stage exclusively into the inactive slot with per-file download,
SHA-256 verification, and slot-local persisted progress (resumable). After full
verification, activation is a single transaction on the
lumen-systemDB flipping the active pointer + recording version/verification metadata. Post-activation readback spot-check; failure triggers automatic rollback to the previous slot. Previous slot retained for user-initiated restore until next staging or GC policy. Organizer-side rollback = republish old content at a new higher version (runbook). Downloads run in the page context (C-21), never the SW. - Alternatives considered:
- In-place overwrite with journaling — rejected: far more failure modes; IDB gives cheap whole-database slots, so journaling complexity is unnecessary.
- Version-keyed stores with GC (no fixed slots) — rejected: unbounded store proliferation complicates quota reasoning; A/B bounds storage at 2× dataset.
- SW-driven download + Cache API staging — rejected: iOS terminates SWs aggressively; page-context downloads survive SW death and can show progress/resume UX.
- Evaluation criteria: invariant compliance under kill-at-any-point; storage overhead; resumability; implementation simplicity; rollback support.
- Reasoning: The pointer-flip transaction is the smallest possible atomic boundary; keeping all writes on the inactive slot makes the invariant trivially arguable. All nine failure stages map to "old dataset untouched" or "rollback to old" (ARCHITECTURE-DESIGN §18.2).
- Consequences: 2× worst-case dataset storage (bounded by budgets ≤40 MB → ≤80 MB worst case; GC trims); staging progress schema needed; activation metadata is the single source of readiness truth.
- Risks: quota pressure during staging with both slots full (mitigation: GC trigger before staging; pre-check free space); pointer-flip transaction failure on pathological IDB state (mitigation: retry once → keep old → diagnostics).
- Reversibility: The slot mechanism is internal to data/sync layers; can be replaced without touching features. Medium reversibility.
- Related: ADR-004, ADR-005, ADR-010, ADR-013.
ADR-007: Emergency baseline
- Status: Accepted — addendum after SPIKE-06 (ARCHITECTURE-VALIDATION.md §2). Fixed tier contents, merge rules, and zero-IDB forward-tolerant floor path are now normative.
- Validation addendum (SPIKE-06): Tier-1 floor JSON shape fixed (
SPIKE-06:12life-safety minimum, ≤16 KB), tier-2 full detail, tier-3 reserved signed ephemeral notices. Resolutionrender_emergency()prefers highest compatible tier with defensive merge (SPIKE-06:60); F-1 hardening: floor renderer forward-tolerant and reachable with zero IDB access for BASELINE_ONLY/eviction (SPIKE-06:75). Same-source generation of floor + dataset section prevents drift (ARCHITECTURE-DESIGN.md:345). - Context: Invariant 2; DISCOVERY OF-3/EM-2; emergency must survive even total storage loss.
- Problem: Decide whether and how to embed emergency information in the app shell in addition to the dataset.
- Decision: Three tiers. Tier 1 — embedded baseline: a minimal emergency record (emergency number + dial action, festival address, coordinates, security contact, first-aid/AED summaries, muster points, exit summaries, core procedures) compiled into the app shell at build time, generated from the same emergency source sheet that produces the dataset section (no drift). Immutable per shell build; ≤16 KB; frozen schema. Tier 2 — dataset emergency section: full detail, signed, updateable. Tier 3 — optional live notices: reserved seam, not V1. Rendering prefers the highest verified tier; provenance always labeled.
- Alternatives considered:
- Dataset-only emergency — rejected: fails invariant 2 under eviction / failed update / incompatible dataset.
- Baseline hand-maintained separately from dataset content — rejected: drift risk between baseline and dataset is a safety issue; single-source generation removes it.
- Baseline in localStorage instead of compiled-in — rejected: localStorage dies with storage eviction too; only shell bytes survive everything.
- Evaluation criteria: survival under every failure mode; content drift risk; update latency tolerance; size cost.
- Reasoning: Emergency is the one feature where "degraded but present" is unacceptable; embedding in immutable shell bytes is the only mechanism that survives total storage loss. Generating it from the same source eliminates the classic two-copies drift hazard.
- Consequences: baseline updates require a shell release (slower cadence — accepted, and labeled); shell build pipeline gains a baseline-generation step; emergency rendering has a no-IDB path.
- Risks: baseline content frozen at build time while contacts change (mitigation: Tier 2 updates + provenance labels + organizer physical channels); baseline scope creep (mitigation: hard 16 KB cap and content policy).
- Reversibility: Additive; tiers can be extended without removing the floor.
- Related: ADR-001, ADR-005, ADR-013, ADR-014.
ADR-008: Map architecture
- Status: Accepted (YELLOW — device-conditional) — validated by SPIKE-03; production readiness conditional on device protocol in SPIKE-03 §7 (see ARCHITECTURE-VALIDATION.md §2, §4, §7)
- Validation addendum (SPIKE-03 F-1,F-3,F-4 — normative): F-1 replaces 4096 cap: overview ≤1600 px longest edge, detail ≤3072 px, total decoded ≤~35 MB, at most one detail resident (OER; swaps overview out) (
SPIKE-03:58); F-3 lazy object-URL lifecycle (create per level, revoke on switch); F-4 dark-modebrightness(.72) saturate(.85)+ optional night asset hookmap-base/overview-night(SPIKE-03:88); WebP 28 MB budget stands with margin. Flip conditions + fallbacks (overview-only, 2×2 tile-split) predefined. - Previous pending: real-art bake-off and real map art (OQ-6) — now tracked as device/content validation, not a Proposed decision.
- Context: DISCOVERY AD-7, AMB-3; no online tiles allowed; GPS never required; low-end devices; organizers most naturally produce illustrated raster art.
- Problem: Choose the offline map representation and interaction model.
- Decision: Raster WebP base map (≤2 zoom levels: overview + optional detail) + data-driven DOM POI overlay + CSS-transform pan/zoom + first-class Facilities list view. POIs carry normalized coordinates in map.json; markers are real DOM buttons; detail level ≤4096px per side, lazy-decoded; no geolocation in V1 (schema hook preserved).
- Alternatives considered:
- Single vector SVG map — crisp and small for schematic maps, but: organizer art is typically illustrated raster; complex SVG pan/zoom perf on low-end devices is unpredictable; large-SVG accessibility is awkward. Would be reconsidered if organizers supply vector art and SPIKE-3 shows SVG perf is fine (DD-9 adjacent).
- Canvas rendering — good perf, but: manual hit-testing, poor accessibility (needs parallel text tree anyway → the list view already provides it), more code.
- Pre-cached raster tile pyramid — rejected: tile bookkeeping complexity without need; venue-scale map fits 2 full-image levels under budget.
- Online tiles with offline cache — rejected by constraint (no core dependence on online tiles).
- Evaluation criteria: offline completeness; low-end perf; accessibility; file size; authoring workflow; interactive markers; search integration; maintainability.
- Reasoning: GPU-scaled image transforms give consistent pan/zoom performance independent of POI count; DOM markers give free accessibility and tap targets; the Facilities list doubles as the screen-reader path and the GPS-free "find nearest facility" path; raster matches what organizers actually produce.
- Consequences: image budgets (≤28 MB map art) enforced in pipeline; coordinate authoring step in pipeline (tap-tool or measured coords); detail-level switching logic; no blue dot in V1.
- Risks: art exceeds budget (mitigation: split/drop detail level); transform jank on very old devices (mitigation: SPIKE-3 on low-end device; fallback = overview-only mode).
- Reversibility: Map rendering is isolated in MapService; swapping representation later doesn't touch other features. High reversibility.
- Related: ADR-003, ADR-005, ADR-009.
ADR-009: Time model
- Status: Accepted — addendum after SPIKE-08 (ARCHITECTURE-VALIDATION.md §2). Change F-3 normative; clarification F-4 normative.
- Validation addendum (SPIKE-08 24/24 PASS, V8/ICU): T1–T7 all PASS; UTC + explicit-zone
Intlcorrect offline; precomputeddayKeyeliminates client day math; DST and 30-min zones handled. F-3 (change): sanity-window warning suppressed untilnow ≥ festivalStart − 45dor a skew has been captured — prevents noisy warning for early preparation (SPIKE-08-TIME-MODEL.md:97). F-4 (clarification): onlyIntl.DateTimeFormatwith explicittimeZoneis allowed; no wall-clock string storage, no manual offset arithmetic. JSC parity on iOS and CDNDateobservation remain UNVERIFIED queued for device matrix (§4, §7). - Previous status: Accepted
- Context: DISCOVERY AD-8, R-S5; Now/Next must work offline; device clocks may be wrong.
- Problem: Define storage, rendering, and correctness strategy for time, including wrong clocks and DST.
- Decision: Events stored as UTC epoch ms. Manifest carries the
festival IANA timezone and festival window. Rendering via
Intl.DateTimeFormatin festival zone (user toggle: device zone). Day boundaries are precomputed at publish time (dayKey). A ClockService computesnow(): persisted server-time offset (captured from HTTPDateheader during any sync) corrects device time; a monotonic anchor detects mid-session clock changes; a sanity window (festival window ±45 days) triggers "check your clock" warnings. Never-online devices use device clock with the sanity heuristic only. - Alternatives considered:
- Store/render local festival wall-clock strings — rejected: ambiguous across device zones, breaks instant arithmetic for Now/Next and overlaps.
- Require network time (NTP-like endpoint) — rejected: violates offline invariants.
- Trust device clock unconditionally — rejected as sole strategy: wrong clocks silently break Now/Next; the offset capture is nearly free.
- Continuous background clock sync — rejected (C-19).
- Evaluation criteria: offline correctness; wrong-clock resilience; DST safety; implementation simplicity.
- Reasoning: UTC storage + IANA rendering is the standard correct answer;
the offset mechanism adds real wrong-clock resilience at trivial cost;
precomputed
dayKeyremoves client-side day-boundary ambiguity entirely. - Consequences: sync responses must expose a server timestamp (HTTP Date is sufficient); clock warnings are part of UX; residual risk for never-online wrong clocks is declared and accepted (absolute times remain readable).
- Risks: organizer supplies wrong IANA zone (mitigation: pipeline validation + fixture tests for DST edges); offset poisoned by a wrong server clock (mitigation: sanity window; offset only shifts display logic, never stored event data).
- Reversibility: Storage format (UTC ms) is stable regardless of rendering policy changes. High reversibility for rendering policy.
- Related: ADR-001, ADR-005, ADR-010.
ADR-010: Synchronization
- Status: Accepted
- Context: DISCOVERY A-07/A-12, SY-* questions; no accounts; connectivity is enhancement-only.
- Problem: Define if/how the app talks to servers.
- Decision: Pull-only synchronization: on app open, on
onlinehint, and on manual request. The only sync operations: fetchlatest.json, fetch candidate package, stage/verify/activate (ADR-006). Versions are monotonic; clients never accept a network downgrade. No uploads, no user-state sync, no background sync registration, no timers, no push in V1. Announcements schema reserved but unimplemented (DD-1). - Alternatives considered:
- Periodic background sync — rejected: unsupported on iOS (C-19) and unnecessary since sync-on-open covers all needs.
- Push-based updates — rejected for V1: iOS push requires install + region/permission fragility; adds server infra; the pull model already guarantees eventual consistency whenever the user opens the app.
- WebSocket/long-poll live channel — rejected: server state + connection management for no V1 requirement.
- Evaluation criteria: platform support on both targets; server ops burden; privacy; invariant 10 compliance.
- Reasoning: Pull-only on static files achieves every V1 need with zero server state and zero permissions; everything more exotic fails the simplicity criterion.
- Consequences: update latency = time-until-next-open-online (accepted and documented); organizers needing urgency use physical channels (runbook).
- Risks: critical schedule change reaches users late (mitigation: changed-event markers, dataset timestamps, organizer comms redundancy).
- Reversibility: Additive — push/announcements can be layered on the same transport seam later without changing pull. High reversibility.
- Related: ADR-005, ADR-006, ADR-011, ADR-014.
ADR-011: Networking abstraction
- Status: Accepted
- Context: Invariant 11–12; DISCOVERY §17; brief requires a clean future transport seam without V1 mesh contamination.
- Problem: Decide whether a transport abstraction belongs in V1 at all, and if so its shape.
- Decision: A minimal, byte-oriented Transport interface under the Sync
Service:
isAvailable(),fetchPointer(edition),fetchBytes(path). V1 ships exactly one implementation (HTTP over the CDN). The Verifier — not any transport — decides authenticity. Features/UI never import sync or transport modules (boundary B-3). No registry, no plugin system, no transport negotiation: the seam is the interface plus the layering rule. - Alternatives considered:
- No abstraction until a second transport exists (YAGNI-pure) — reasonable, but the interface costs ~one small module and prevents the much costlier retrofit of untangling fetch calls from feature code; accepted only in this minimal form.
- Message/sync-semantics abstraction (CRDT op log, gossip protocol interfaces) — rejected for V1: speculative; encodes mesh assumptions (violates principle 14); a byte transport + signed payloads covers every conceivable future transport, since packages are immutable signed objects.
- Adapter over fetch scattered in services — rejected: that is the leak invariant 12 forbids.
- Evaluation criteria: cost in V1; leak-proofness; sufficiency for plausible future transports (LAN mirror, native-companion bridge, mesh gateway).
- Reasoning: Any future delivery mechanism — including mesh gateways and native companions — can present "fetch these bytes"; verification stays client-side. That makes the byte interface the least-speculative seam that satisfies the requirement.
- Consequences: SyncService is transport-parameterized; tests can inject a fault transport (fault injection for §18.2 stages); UI shows no transport identity.
- Risks: under/over-specification discovered when a real second transport appears (mitigation: interface is deliberately narrow; versioning it is cheap).
- Reversibility: High — interface is internal.
- Related: ADR-006, ADR-010, ADR-012, ADR-013.
ADR-012: Future mesh strategy
- Status: Accepted (strategy only; nothing implemented)
- Context: Brief: mesh is future-only; DISCOVERY §17 concluded browsers alone almost certainly cannot host a meaningful mesh (no BLE peripheral, no ad-hoc Wi-Fi, no background sockets; iOS lacks Web Bluetooth).
- Problem: Define how future mesh could attach without contaminating V1, and what V1 must do now to keep that possible.
- Decision: V1 does exactly three things for future mesh: (1) the Transport seam of ADR-011; (2) the signed-payload rule — bytes from any transport are inert until the Verifier passes them, so untrusted peers can never inject content; (3) package/announcement formats are immutable, content-addressed, version-monotonic — properties any store-and-forward or gossip transport needs. All mesh concerns (peer discovery, routing, dedupe, replay beyond version monotonicity, peer auth, message size adaptation, emergency broadcast semantics) are explicitly future work. A future mesh is assumed to require a native companion, hardware relay, or organizer LAN service; if that proves impossible, V1 loses nothing (HTTP is complete functionality, not a placeholder).
- Alternatives considered:
- Design a mesh message schema now — rejected: encodes assumptions about a technology not chosen, violating principle 14.
- WebRTC data-channel groundwork in V1 — rejected: signaling needs connectivity anyway; iOS constraints; zero V1 value; pure complexity.
- Do nothing at all (no seam) — rejected: the brief requires an extension point; the seam chosen costs one interface.
- Evaluation criteria: V1 cost; leak-proofness; plausibility under multiple future mesh shapes (BLE mesh via companion, LAN mirror, WebRTC via gateway).
- Reasoning: The cheapest correct move is to make Lumen's data plane transport-agnostic and authenticity-strict, then stop. Every concrete mesh capability browsers lack lives outside the app anyway.
- Consequences: documentation-only artifacts in V1; future mesh work starts with a transport implementation + a feasibility study of the out-of-browser component.
- Risks: the seam proves slightly wrong for a real mesh (mitigation: narrow interface, cheap to version); false expectation that mesh is "easy later" (mitigation: this ADR documents the out-of-browser likelihood).
- Reversibility: Fully — nothing is built.
- Related: ADR-011, ADR-013.
ADR-013: Security model
- Status: Accepted as a model — crypto library choice remains Proposed (YELLOW) pending audit (ARCHITECTURE-VALIDATION.md §2, §7); overall threat model GREEN. Verifier choice (WebCrypto SHA-256 + bundled pure-JS Ed25519) validated as correct per TQ-9/SQ-1 but supply-chain audit is still required (AQ-06).
- Validation note: SPIKE-04/02 ordering (signature before hash), per-file SHA-256, monotonic anti-replay, CSP
self-only, and data-minimization posture all reconciled; no spike contradicts the model. Key custody/rotation drill (AQ-21, S-01) remains ops gate. - Context: DISCOVERY §18; invariants on data integrity; emergency content is safety-relevant; data minimization mandated.
- Problem: Define integrity, authenticity, transport security, content safety, and privacy posture without unnecessary complexity (no accounts).
- Decision:
- Package signing: Ed25519 signature over SHA-256 of the exact manifest bytes; per-file SHA-256+size in manifest; client embeds a fingerprinted public key set (rotation-capable). Verification implemented with a small, audited, pure-JS Ed25519 verifier bundled in the shell (WebCrypto Ed25519 is not guaranteed on the iOS 16.4 baseline) — library choice Proposed pending audit check.
- Transport: HTTPS-only + HSTS; single origin; immutable package URLs.
- Downgrade/replay protection: monotonic packageVersion acceptance; organizer rollback only via new version.
- Content safety: structured-data rendering only (no raw HTML); CSP without inline script/eval; no third-party resources.
- Privacy: no accounts, no telemetry, no location capture; diagnostics local, scrubbed, user-initiated.
- Ops: signing key offline; singular publisher role; repo audit trail; key-loss runbook (ship new key via shell update).
- Alternatives considered:
- TLS-only trust (no signatures) — rejected: doesn't protect against hosting/CDN compromise or publish-pipeline mistakes; emergency content deserves defense in depth.
- WebCrypto-only Ed25519 — rejected: baseline iOS gap (DISCOVERY TQ-9); the bundled verifier removes the gap deterministically.
- Full PKI/certificate-style scheme, JWT-based updates, or authenticated user sessions — rejected: complexity without a V1 threat that requires it (principle: realistic over theoretical).
- Evaluation criteria: coverage of realistic threats (TH-1…TH-6); key management practicality for a small team; client cost; privacy posture.
- Reasoning: Signature + per-file hashes + monotonic versions defeat the realistic high-impact threats (tampered/malicious packages, emergency spoofing, replay) with minimal moving parts; data minimization removes whole threat classes rather than defending them.
- Consequences: signing tooling + key custody in ops; every dataset byte verified before activation (cost: seconds at staging time); CSP strictness shapes build output (hashed scripts only).
- Risks: signing key loss (runbook + key set rotation); verifier library supply-chain risk (mitigation: pinned, audited, tiny; TH-11); strict CSP friction during build (accepted).
- Reversibility: Signature scheme can be strengthened (key set mechanism); removing security would be a regression, not a reversal.
- Related: ADR-005, ADR-006, ADR-010, ADR-011, ADR-012.
ADR-014: Deployment strategy
- Status: Accepted on topology — provider choice remains Proposed (YELLOW) pending selection (AQ-14) (ARCHITECTURE-VALIDATION.md §2, §7)
- Validation note: Static CDN, single origin, immutable versioned URLs +
latest.jsonpointer + staging/production origins + cache headers per ARCH §24 are all validated; provider is the only unresolved piece and is hard to reverse post-users (origin-keyed storage) so provisional staging origin is correct for development. - Previous: Accepted (provider Proposed pending hosting choice — AQ-14)
- Context: DISCOVERY AD-9/AD-10/AD-14; A-17 (small team); HTTPS required; all content is static signed files.
- Problem: Choose hosting topology, release topology, and publishing flow.
- Decision: Static-only deployment on an HTTPS CDN, single origin.
Layout:
/(index,no-cache),/assets/<hash>(immutable),/latest.json(short-lived),/editions/<ed>/packages/<v>/**(immutable). Publishing: content repo → automated pipeline (validate → build sections → hash → sign → upload → pointer update → smoke check). Two environments: staging origin and production origin, same code, different origins. App shell builds are separate releases from dataset publishes (independent cadence, compatibility envelope per ADR-005/§18.6). No dynamic backend. - Alternatives considered:
- Small dynamic server (for latest.json logic, telemetry, push) — rejected: no V1 feature needs server logic; ops burden fails A-17.
- Self-hosted single server — possible but: CDN gives free resilience + edge caching for pre-festival load spike; self-hosting concentrates outage risk (RK-10). Reconsider only if cost/access constraints demand.
- Object storage without CDN front — equivalent in simplicity; CDN edge preferred for the pre-festival install spike; provider choice left open.
- Evaluation criteria: ops burden; resilience during festival; cost; publish safety (staging → prod); HTTPS/headers control.
- Reasoning: Everything the system serves is immutable bytes or one tiny mutable pointer; static hosting is the exact fit and keeps the failure surface to "files in or out".
- Consequences: publisher credentials live at pipeline/CDN layer; publish runbooks required; cache headers are part of the contract; staging edition for tests.
- Risks: CDN outage blocks updates during festival (impact low — devices hold last-good data; RK-10); provider lock-in minimal (plain files).
- Reversibility: Provider swap is a DNS/origin change with storage consequences for existing users (origin-keyed storage!) — an origin change strands installed data; therefore pick the production origin deliberately and early (AQ-18 blocks production setup, not development).
- Related: ADR-002, ADR-005, ADR-013.
Decision dependency map
flowchart TD
A1[ADR-001 Offline-first] --> A2[ADR-002 PWA strategy]
A1 --> A4[ADR-004 Local storage]
A1 --> A5[ADR-005 Festival Data Package]
A1 --> A10[ADR-010 Sync pull-only]
A2 --> A14[ADR-014 Deployment]
A4 --> A6[ADR-006 Atomic updates]
A5 --> A6
A5 --> A7[ADR-007 Emergency baseline]
A5 --> A9[ADR-009 Time model]
A5 --> A8[ADR-008 Map]
A5 --> A13[ADR-013 Security]
A6 --> A11[ADR-011 Transport seam]
A10 --> A11
A11 --> A12[ADR-012 Mesh strategy]
A13 --> A12
A3[ADR-003 Vanilla TS] --> A8