Architecture Decisions
Section 01

Architecture Decisions

This document records the architecture decisions that are binding for v1 delivery. It is derived from the locked stack specification and the product brief; where wording differs, the locked implementation decisions in /spec/ win.

Source inputs

1. Authority and change control

  • This document is normative for engineering, product, and content-pipeline implementation choices in v1.
  • Any change that affects system boundaries, trust boundaries, the data model, approved dependencies, offline guarantees, monetisation model, or cultural review flow requires a new ADR entry in this document.
  • Open questions stay in /spec/20-open/. They do not silently weaken a locked architecture decision.

2. System boundaries

Ochre & Soul is a thin-client mobile product. The client owns presentation, interaction, offline cache, and local progress. The backend owns identity, catalogue authority, entitlements, premium gating, and publishing controls. Artwork production and review live in a separate content pipeline.

flowchart LR
    subgraph Mobile["Mobile app boundary"]
        UI["UI shell<br/>Flutter + Riverpod + GoRouter"]
        Canvas["Canvas engine<br/>CustomPainter + hit testing"]
        Local["Local persistence<br/>Drift + file cache + secure storage"]
        Telemetry["Telemetry<br/>PostHog + Sentry with child-mode policy"]
    end

    subgraph Backend["Backend boundary"]
        API["Catalogue APIs<br/>PostgREST-style reads"]
        Fn["Privileged functions<br/>purchase validation, signed URLs, sync, admin"]
        DB["PostgreSQL + RLS"]
        Storage["Object storage + CDN"]
        Auth["Auth + anonymous identity"]
    end

    subgraph Pipeline["Content pipeline boundary"]
        Authoring["Artwork authoring"]
        Segmentation["Segmentation + bundle generation"]
        Review["Cultural review"]
        Publish["Upload + publish workflow"]
    end

    Mobile --> Backend
    Pipeline --> Backend

2.1 — Component layers inside the app

LayerResponsibilityMust not do
app/App bootstrap, routing, dependency wiring, environment configBusiness logic or feature-specific persistence
features/User-facing feature modules such as library, canvas, progress, auth, paywall, settingsReach across into unrelated features without a shared contract
core/Shared infrastructure: network, storage, analytics, config, purchase helpersAccrete feature behaviour that belongs in a feature module
Canvas runtimeRendering, hit testing, palette interaction, fill feedback, viewport transformsFetch directly from admin APIs or make network-dependent play decisions
Local data layerDrift models, sync queue, artwork cache, receipt cache, secure token storageDecide entitlement truth or bypass server merge rules

2.2 — Trust boundaries

  • The client never decides premium access, role escalation, receipt validity, or publication status.
  • Service-role credentials never ship in the app.
  • Premium asset access is granted only via server-issued signed URLs or public CDN paths for free artworks.
  • Reviewer and admin capabilities are server-gated function surfaces, not hidden client menus.

3. Approved stack and dependency policy

3.1 — Approved platform choices

The approved stack is the locked one in /spec/05-stack/ §5.1–§5.3. The table below names the choices that have direct architectural consequences; anything not listed defaults to the spec.

ConcernApproved choiceWhy it is binding
Mobile frameworkFlutter + DartThe canvas engine is the highest-risk part of the product and the current spec is written around Flutter rendering and profiling constraints.
App architectureFeature-first clean architectureKeeps feature code isolated while letting core/ hold shared infrastructure only.
State managementRiverpod 2.xChosen state model for a complex offline-first client.
RoutingGoRouterSupports declarative navigation and deep links without introducing a second routing pattern.
NetworkingDioStandardises interceptors, retries, auth propagation, and transport logging.
Local structured storageDriftApproved local database for syncable state and metadata.
Secure secretsflutter_secure_storageRequired for auth tokens and receipt cache.
File and bundle cachepath_provider + app-support directorySupports the local-first bundle model and offline play.
In-app purchase clientin_app_purchaseAligns with StoreKit 2 and Google Billing in the approved stack.
Backend platformInsForgeConsolidates auth, database, storage, and privileged functions under one trust model.
Authoritative databasePostgreSQL with RLSRLS is the primary authorisation layer; it is not optional.
Asset deliveryInsForge Storage behind a CDNSupports public starter content and signed premium delivery.
ObservabilitySentry + PostHogApproved only within the privacy rules defined for child mode.
Remote configGrowthBook or equivalentAllowed only for staged rollout and paywall controls, not for undermining locked product scope.

3.2 — Dependency rule

  • New runtime dependencies are allowed only when they fit the existing layers and do not duplicate an approved capability.
  • A new dependency that introduces a competing architecture path, a second backend, or a second state model is rejected unless a later ADR explicitly supersedes this document.
  • Tooling dependencies for the content pipeline are acceptable when they do not leak into the mobile runtime or weaken the publish-review controls.

4. Approved implementation patterns

  • Thin-client mobile app with remote catalogue authority and local cached artwork bundles.
  • Anonymous-first identity from first launch — every install holds a persistent anonymous session (see /spec/03-glossary/ §3.3) with later promotion to authenticated status on the same profile where possible.
  • Local-first progress writes with asynchronous sync and retry.
  • Server-side write surfaces for privileged or trust-sensitive operations.
  • Binary artwork bundle delivery and local cache reuse for offline play.
  • Single CustomPainter canvas surface with spatial-index hit testing.
  • Cultural review as a publish gate, not an editorial afterthought.
  • Quantitative phase gates before wider implementation commitments.

5. Disallowed patterns and forbidden dependencies

5.1 — Disallowed patterns

  • One-widget-per-region canvas rendering.
  • Any gameplay interaction that waits on a network round-trip.
  • Client-side purchase validation, entitlement authority, or premium access checks.
  • Last-write-wins progress sync.
  • JSON-based region-completion storage for the primary progress record.
  • Sign-up walls before the user reaches value on a free starter artwork.
  • Behavioural analytics beyond app_opened and crash signals when child_mode = true.
  • Publishing artwork without recorded cultural review approval.
  • Subscriptions, ads, social features, or AI artwork generation in v1 delivery.
  • Hidden admin capability implemented only in the client without matching server enforcement.

5.2 — Forbidden or unapproved dependencies for v1

Dependency or classStatusReason
provider or mixed state-management stacksForbiddenRiverpod is the approved state model; mixing patterns increases cognitive and runtime complexity.
Isar as the primary local databaseForbiddenDrift is the selected local store in the locked stack.
Firebase Auth / Firestore / Storage as a parallel app backendForbiddenSplits trust, identity, and data authority away from the approved InsForge architecture.
Ad network SDKsForbidden in v1Ads are not approved for launch and would conflict with child-mode privacy constraints.
Subscription-management abstractions that replace the server entitlement modelForbidden in v1Launch monetisation is pack-based and receipt truth stays on the backend.
A second mobile analytics product beyond approved observability toolingUnapprovedIncreases privacy and compliance surface without v1 justification.

6. ADR register

ADR-001 — Adopt a Flutter thin-client mobile architecture

Status : Accepted

Decision : Build Ochre & Soul as a Flutter iOS and Android app with a thin-client architecture. The client renders the experience and caches downloaded assets locally; the backend remains the authority for catalogue, identity, entitlements, and publishing state.

Rationale : The product’s differentiator is the colouring experience itself, not heavy server-orchestrated UI. A thin client keeps the app small, supports offline play after download, and fits the need for a custom canvas runtime.

Consequences : The mobile client carries meaningful rendering and offline-state complexity. Backend contracts must stay stable and asset formats must be explicit.

ADR-002 — Standardise the app on feature-first clean architecture

Status : Accepted

Decision : Organise the app by feature modules under features/, with shared infrastructure under core/ and bootstrap concerns under app/.

Rationale : The app spans catalogue, download, canvas, progress, auth, paywall, and admin-preview concerns. Feature-first structure reduces coupling and keeps canvas-specific complexity from leaking across the codebase.

Consequences : Shared abstractions must remain genuinely shared. “Core” cannot become a dumping ground for feature logic.

ADR-003 — Make the canvas engine a single-surface renderer

Status : Accepted

Decision : Use a single CustomPainter per canvas, cached outline and fill layers, clipped label drawing, and a spatial-index hit-test path.

Rationale : The Phase 0 performance gates are incompatible with a deep widget tree per region. A single-surface renderer keeps frame-time, memory, and tap latency within a plausible v1 budget.

Consequences : Canvas behaviour is specialised code and must be profiled, tested, and guarded by fixture-based benchmarks from Phase 0 onward.

ADR-004 — Keep progress local-first and represent fills as bitsets

Status : Accepted

Decision : Store per-artwork progress as a bitset in user_artwork_progress.completed_regions, sync asynchronously, and merge local and remote state by bitset union.

Rationale : Region completion is monotonic. Union merge is simpler and more correct for this domain than timestamp-based conflict resolution, while bitsets keep storage small enough for large artworks.

Consequences : Progress writes remain lightweight and offline-friendly, but tooling and tests must understand the bitset representation. Reverting completed regions is not part of the core data model. See /spec/07-data-layer/ §7.4 for the bitset merge specification.

ADR-005 — Use anonymous-first identity with server-side promotion

Status : Accepted

Decision : Every install starts with an anonymous authenticated session and a profiles row. Sign-up is deferred until purchase or cross-device continuity is needed.

Rationale : The brief prioritises time-to-aha and explicitly rejects early sign-up friction. Anonymous-first identity preserves progress and preferences while keeping the app authenticated enough for RLS-protected reads.

Consequences : Promotion and account-linking flows are critical server functions. Purchase and restore flows must clearly explain why sign-in is required at that point.

ADR-006 — Centralise trust-sensitive operations on InsForge

Status : Accepted

Decision : Use InsForge PostgreSQL, Auth, Storage, and Functions as the sole backend authority. Read paths may use generated APIs; privileged writes and sensitive logic must go through server functions with the service role.

Rationale : A single backend trust model reduces integration sprawl and lets RLS enforce most access rules close to the data.

Consequences : Function design, RLS policy quality, and operational discipline become architecture-critical. The client must not develop “temporary” bypass paths.

ADR-007 — Gate premium assets with entitlements and signed URLs

Status : Accepted

Decision : Free artworks may use public CDN URLs. Premium artworks require a non-anonymous user with an active entitlement and receive short-lived signed URLs from the backend.

Rationale : Asset URLs are otherwise trivial to share. Signed URL issuance aligns premium access with server-side entitlement truth without making free starter content harder to access.

Consequences : Resume flows need signed-URL renewal support, and entitlement bugs become directly visible to the user during download attempts. See /spec/07-data-layer/ §7.5 for the entitlement model and Family Sharing propagation, and /spec/09-user-flows/ §9.5 for the premium access flow.

ADR-008 — Launch with one-off content packs only

Status : Accepted

Decision : v1 monetisation is free starter artworks plus one-off paid content packs. Subscription is explicitly deferred post-MVP.

Rationale : Pack-based monetisation keeps launch scope narrower and matches the calmer, less game-like positioning. It also avoids bringing subscription lifecycle complexity into the first release.

Consequences : The paywall, entitlement model, analytics, and store configuration all target packs rather than recurring billing. Any subscription work needs a superseding ADR. See /spec/04-features/ §4.7 for the locked monetisation decision and /spec/20-open/ OD-02 for the resolution record.

ADR-009 — Treat cultural review as a technical publish gate

Status : Accepted

Decision : No artwork may be published until its cultural review status is approved and the review is recorded.

Rationale : Cultural integrity is a product differentiator and a trust requirement, not optional editorial polish. Treating review as a system gate prevents schedule pressure from weakening the standard later.

Consequences : The content pipeline, admin tooling, and database schema must preserve review state and block publish operations accordingly.

ADR-010 — Use Phase 0 performance gates to decide whether v1 is feasible on the chosen stack

Status : Accepted

Decision : The project does not progress beyond the proof-of-concept stage unless all eight Phase 0 thresholds pass on the same release build and device class.

Rationale : The main delivery risk is the canvas engine, not catalogue CRUD. Committing to later phases before proving rendering feasibility would be an avoidable planning error.

Consequences : Phase sequencing is intentionally conservative. Backend, monetisation, and content-pipeline work can be prepared, but they do not overrule a failed rendering proof. The eight gates from /spec/17-phases/ Phase 0 are: cold start to library ≤ 1.8 s, cached artwork open ≤ 600 ms, steady-state frame time ≤ 16 ms p95, zero frame excursions > 50 ms, tap-to-fill latency ≤ 50 ms p95, tap accuracy ≥ 99% at 4× zoom, resident memory ≤ 220 MB on F-stress, and battery drain ≤ 6% per 30 min on F-typical — all must pass on the same release build on a Pixel 6a-equivalent mid-range Android.

ADR-011 — Adopt the CLRX v1 binary artwork bundle format

Status : Accepted

Decision : Use the binary bundle format specified as CLRX v1 in /spec/06-data-model/ §6.2 as the sole on-disk and over-the-wire representation of an artwork. A bundle comprises a JSON manifest, brotli-compressed binary region payload, preview, thumbnail, optional line-art reference, and a version file. Bundles are atomically written, integrity-checked on parse, and evicted per the lifecycle rules in /spec/06-data-model/ §6.6.

Rationale : A single binary format keeps download size, parse cost, and cache footprint within the Phase 0 budget. JSON for region geometry would inflate bundle size and parse time beyond the thresholds enumerated in ADR-010. A typed binary format also lets the parser exit early on integrity failures rather than fail mid-render, which matters for offline reliability.

Consequences : The bundle format becomes an architecture-critical contract between the content pipeline, backend storage, and mobile parser. Format changes require a version bump and a forward-compatible parser. The eviction and atomic-write rules in §6.6 must be enforced exactly to avoid partial-bundle states under churn or interrupted downloads.

ADR-012 — Bake accessibility into the colouring engine, not a settings page

Status : Accepted

Decision : Accessibility-critical behaviour is implemented inside the canvas runtime and palette widget, not added later as a settings-page overlay. The engine ships with always-show-numbers on by default, a high-contrast number halo at every zoom level, optional pattern fills distinct per colour, screen-reader semantics for both palette and canvas, reduce-motion compliance, and “snap to nearest small region” within a 16 px radius. Palettes are validated against deuteranopia, protanopia, and tritanopia simulations by the bundler before publication. See /spec/12-accessibility/ and /spec/22-principles/ principle 4.

Rationale : A colouring app must be usable by people who cannot reliably distinguish colours; numbers are the accessibility fallback, and they must remain legible at every zoom level. Deferring this to a settings toggle invariably means the canvas is built around colour-only interaction first and patched later, which the brief and §22-principles explicitly reject as a positioning failure.

Consequences : Canvas implementation cost is higher up front (palette validation, halo rendering, pattern-fill mode, semantics, tap-target snapping). Phase 0 fixtures must exercise these paths, not just the default render. Number labels scale with artwork zoom, not system font scale — this is a deliberate canvas-level choice that the Dynamic Type policy on non-canvas screens does not override.

7. Feasibility constraints shaping v1

ConstraintWhy it mattersv1 effect
Canvas performance on mid-range AndroidThis is the primary technical risk and the product’s core interaction.Phase 0 gates block progression if missed; no heavy animation or widget-per-region approach is allowed.
Offline-first play after downloadThe calm promise fails if colouring depends on connectivity.Downloaded artworks must remain fully playable offline; sync is deferred and retried.
Child-mode privacy and platform family rulesAnalytics, purchases, and account behaviour are constrained by compliance obligations.Ads are excluded from v1; behavioural analytics are heavily restricted in child mode; purchases rely on OS-level parental controls.
Cultural review throughputThe launch library depends on repeatable review and publish discipline.Tooling-only admin is acceptable in MVP, but the review gate itself is not optional.
Premium entitlement correctnessPaid content becomes unusable if validation and gating are unreliable.Receipt validation stays server-side; signed URLs and restore flows are mandatory, not polish.
Accessibility built into the canvasNumbers and contrast are the only fallback for users who cannot reliably distinguish colours; deferring them turns the engine into a colour-only-first product.Always-show-numbers on by default, palette validated against deuteranopia/protanopia/tritanopia at bundle time, optional pattern fills, tap-target snapping inside the engine. See ADR-012 and /spec/12-accessibility/.
Scope disciplineThe project already has meaningful engine, backend, and content-pipeline complexity.Subscriptions, advanced child profiles, multiplayer, UGC, AI generation, and a full admin dashboard stay out of MVP.

8. Practical rules for implementation reviews

  • Reject any proposal that adds a second backend authority or bypasses RLS for ordinary client reads and writes.
  • Reject any canvas change that adds per-region widgets, unbounded repaints, or extra abstraction layers without measurement.
  • Reject any product flow that delays the first satisfying fill behind account creation or a purchase request.
  • Reject any publishing workflow that can mark content live without cultural-review approval being recorded.
  • Treat Phase 0 benchmark regressions as architecture issues, not routine polish tickets.