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.
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 compositiondomain/: business models, policies, application actions, repository contractsdata/: Drift schema + concrete repository adapters + persistence mechanics
domain/ is the dependency target. It must not import from ui/ or data/.Directory Responsibilities
domain/application/*: operation-level orchestration (“do one thing”)domain/repositories/*: abstract contracts (stable seams)domain/mana/*: pure mana domain logic and deterministic helpersdata/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, notuiordata.
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
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
- SQL/Drift queries
- transaction boundaries
- persistence model to domain mapping
- event append semantics
- 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
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 behaviortest/domain/application: validation, failure mapping, orchestrationtest/domain/mana: pure domain formulas/policiestest/ui: provider + view model wiring
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
- Add/extend contract in
domain/repositories - Implement in
data/repositories - Add
Get*orWatch*action indomain/application/<feature> - Wire provider(s) in
ui/core/providers - Consume provider in feature view model/screen
New command capability
- Extend repository write API
- Implement write semantics in data adapter
- Add mutation action (validation + logging + failure mapping)
- Inject action into view model
- Invoke view model method from UI
New domain policy/calculation
- Add policy/model in
domain/<feature>(ordomain/mana/enginefor mana) - Add focused unit tests at domain layer
- Update action orchestration only if required
- Keep data adapters storage-focused
Common Pitfalls (And Correct Placement)
| Pitfall | Why it hurts | Correct place |
|---|---|---|
| Widget runs domain formula | hard to test, duplicates logic | domain/* |
| Action contains SQL/Drift query | blurs boundaries, harder to replace persistence | data/repositories/* |
| Data adapter decides business policy | hidden behavior, brittle coupling | domain/* or domain/application/* |
| UI reads repository directly | bypasses validation/logging/failure policy | action + provider path |
| Domain imports data/ui types | circular coupling risk | map at adapter boundary |
Architecture Decision Heuristic
When adding code, ask:- Is this about what should happen? ->
domain/* - Is this about orchestrating one operation? ->
domain/application/* - Is this about how it is stored/retrieved? ->
data/* - Is this about rendering or interaction flow? ->
ui/*