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
| Layer | Responsibility | Must not do |
|---|---|---|
app/ | App bootstrap, routing, dependency wiring, environment config | Business logic or feature-specific persistence |
features/ | User-facing feature modules such as library, canvas, progress, auth, paywall, settings | Reach across into unrelated features without a shared contract |
core/ | Shared infrastructure: network, storage, analytics, config, purchase helpers | Accrete feature behaviour that belongs in a feature module |
| Canvas runtime | Rendering, hit testing, palette interaction, fill feedback, viewport transforms | Fetch directly from admin APIs or make network-dependent play decisions |
| Local data layer | Drift models, sync queue, artwork cache, receipt cache, secure token storage | Decide 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.
| Concern | Approved choice | Why it is binding |
|---|---|---|
| Mobile framework | Flutter + Dart | The canvas engine is the highest-risk part of the product and the current spec is written around Flutter rendering and profiling constraints. |
| App architecture | Feature-first clean architecture | Keeps feature code isolated while letting core/ hold shared infrastructure only. |
| State management | Riverpod 2.x | Chosen state model for a complex offline-first client. |
| Routing | GoRouter | Supports declarative navigation and deep links without introducing a second routing pattern. |
| Networking | Dio | Standardises interceptors, retries, auth propagation, and transport logging. |
| Local structured storage | Drift | Approved local database for syncable state and metadata. |
| Secure secrets | flutter_secure_storage | Required for auth tokens and receipt cache. |
| File and bundle cache | path_provider + app-support directory | Supports the local-first bundle model and offline play. |
| In-app purchase client | in_app_purchase | Aligns with StoreKit 2 and Google Billing in the approved stack. |
| Backend platform | InsForge | Consolidates auth, database, storage, and privileged functions under one trust model. |
| Authoritative database | PostgreSQL with RLS | RLS is the primary authorisation layer; it is not optional. |
| Asset delivery | InsForge Storage behind a CDN | Supports public starter content and signed premium delivery. |
| Observability | Sentry + PostHog | Approved only within the privacy rules defined for child mode. |
| Remote config | GrowthBook or equivalent | Allowed 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
CustomPaintercanvas 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_openedand crash signals whenchild_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 class | Status | Reason |
|---|---|---|
provider or mixed state-management stacks | Forbidden | Riverpod is the approved state model; mixing patterns increases cognitive and runtime complexity. |
| Isar as the primary local database | Forbidden | Drift is the selected local store in the locked stack. |
| Firebase Auth / Firestore / Storage as a parallel app backend | Forbidden | Splits trust, identity, and data authority away from the approved InsForge architecture. |
| Ad network SDKs | Forbidden in v1 | Ads are not approved for launch and would conflict with child-mode privacy constraints. |
| Subscription-management abstractions that replace the server entitlement model | Forbidden in v1 | Launch monetisation is pack-based and receipt truth stays on the backend. |
| A second mobile analytics product beyond approved observability tooling | Unapproved | Increases 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
| Constraint | Why it matters | v1 effect |
|---|---|---|
| Canvas performance on mid-range Android | This 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 download | The 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 rules | Analytics, 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 throughput | The 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 correctness | Paid 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 canvas | Numbers 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 discipline | The 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.