Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.canthus.org/llms.txt

Use this file to discover all available pages before exploring further.

Why This Architecture Exists

Canthus optimizes for four constraints:
  • Local-first reliability: core workflows must work offline.
  • Deterministic business logic: same inputs should produce the same domain outputs.
  • Replaceable infrastructure: database, logging, and integration details should be swappable.
  • Low-coupling UI: widgets should render state and send intents, not run business rules.
The operating rule is one-way flow: UI -> ViewModel -> Application Action -> Repository Contract -> Data Adapter -> Drift

Layer Model

The app is organized into three concrete code layers.
  • ui/: rendering, interaction wiring, feature view models, provider composition
  • domain/: business models, policies, application actions, repository contracts
  • data/: Drift schema + concrete repository adapters + persistence mechanics
domain/ is the dependency target. It must not import from ui/ or data/.

Directory Responsibilities

app/lib/
  ui/
  domain/
    application/
      common/
      check_ins/
      events/
      mana/
      tasks/
    repositories/
    models/
    services/
    mana/
      engine/
      models/
      support/
  data/
    database/
    repositories/
How to read this split:
  • domain/application/*: operation-level orchestration (“do one thing”)
  • domain/repositories/*: abstract contracts (stable seams)
  • domain/mana/*: pure mana domain logic and deterministic helpers
  • data/repositories/*: contract implementations with SQL/Drift details

Dependency Rules

Practical implications:
  • Widgets do not import Drift repositories directly.
  • Domain models are persistence-agnostic (no Drift row types).
  • Data adapters map persistence rows to-and-from domain models.
  • Business formulas and policy branches stay in domain, not ui or data.

Operation Lifecycle (How It Works)

Read flow

Reads are routed through actions so query shape, logging, and failures are standardized.

Command flow

Writes originate from ViewModels and pass through mutation actions.

Compute flow (pattern for heavy domain logic)

For compute-heavy features (mana is the reference), input assembly and pure computation are separate. Why this split works:
  • SQL concerns stay in adapters.
  • Compute engine remains replayable and unit-test friendly.
  • Action boundary handles orchestration side-effects (for example snapshot updates) in one place.
  • Engine outputs policy state (for example transparency state), while UI maps that state to final user copy.

Application Actions: Purpose and Contract

Application actions are the boundary between UI-driven intent and domain/data execution. Each action should:
  • represent one operation
  • validate caller input
  • call repository contracts/services
  • map exceptions to typed failures
  • emit structured logs
  • return ApplicationResult<T> for explicit success/failure handling
See action result contract in lib/domain/application/common/application_result.dart. Examples of action roles:
  • read: Get*, Watch*
  • mutation: Create*, Update*, Delete*, Complete*
  • compute: Compute*, WatchComputed*

Repository Contracts vs Adapters

Repository contracts define what the domain needs, not how storage works.
  • contracts live in domain/repositories
  • implementations live in data/repositories
Adapters own:
  • SQL/Drift queries
  • transaction boundaries
  • persistence model to domain mapping
  • event append semantics
Adapters do not own:
  • UI logic
  • domain policy decisions
  • formula behavior

Composition Root (Provider Wiring)

ui/core/providers/ is the composition root:
  • repository providers create concrete adapters
  • action providers create one action per operation
  • feature providers call actions
  • screens/view models consume feature providers
This keeps dependency construction centralized and makes feature modules predictable.

Why It Works

1. Change isolation

Most changes are constrained to one layer:
  • SQL changes -> mostly data/*
  • operation contract changes -> mostly domain/application/* + call sites
  • business-rule changes -> mostly domain/*
  • visual changes -> mostly ui/*

2. Testability

Tests mirror architectural seams:
  • test/data: adapter + database behavior
  • test/domain/application: validation, failure mapping, orchestration
  • test/domain/mana: pure domain formulas/policies
  • test/ui: provider + view model wiring
This localizes failures to where they originate.

3. Determinism and replay safety

By isolating pure computation from persistence and UI, deterministic re-computation is straightforward for analytics, migrations, and debugging.

4. Operational observability

Action-level logging standardizes metadata and failure surfaces, which improves triage without leaking sensitive payloads.

Rules for Adding New Code

New read capability

  1. Add/extend contract in domain/repositories
  2. Implement in data/repositories
  3. Add Get* or Watch* action in domain/application/<feature>
  4. Wire provider(s) in ui/core/providers
  5. Consume provider in feature view model/screen

New command capability

  1. Extend repository write API
  2. Implement write semantics in data adapter
  3. Add mutation action (validation + logging + failure mapping)
  4. Inject action into view model
  5. Invoke view model method from UI

New domain policy/calculation

  1. Add policy/model in domain/<feature> (or domain/mana/engine for mana)
  2. Add focused unit tests at domain layer
  3. Update action orchestration only if required
  4. Keep data adapters storage-focused

Common Pitfalls (And Correct Placement)

PitfallWhy it hurtsCorrect place
Widget runs domain formulahard to test, duplicates logicdomain/*
Action contains SQL/Drift queryblurs boundaries, harder to replace persistencedata/repositories/*
Data adapter decides business policyhidden behavior, brittle couplingdomain/* or domain/application/*
UI reads repository directlybypasses validation/logging/failure policyaction + provider path
Domain imports data/ui typescircular coupling riskmap at adapter boundary

Architecture Decision Heuristic

When adding code, ask:
  1. Is this about what should happen? -> domain/*
  2. Is this about orchestrating one operation? -> domain/application/*
  3. Is this about how it is stored/retrieved? -> data/*
  4. Is this about rendering or interaction flow? -> ui/*
If a piece of code answers more than one of those questions, split it.