Architecture & Concepts
Rewhaven keeps a strict, single-direction architecture. Two ideas carry most of the weight: the one data path and the inside/outside split.
The one data path
Every UI-to-storage interaction flows through exactly one chain:
- Widget — dumb leaf UI; no logic, no I/O.
- Bloc / Cubit — state + side effects. Created with
BlocProviderin a page'swrappedRoute(). - Repository — a thin presentation delegate. It adapts the SDK facade to what blocs need; it does not hold domain rules.
- Client (facade) — the
client_sdkpublic surface. The only data-layer type presentation is allowed to know about. - Service — where the domain rules live (the SDK enforces them here).
- Adapter — pure I/O against the local store (Drift) or the cloud (Supabase).
Domain rules live in the SDK Service, not in repositories. Repositories are intentionally thin.
Load-bearing rules (don't violate)
- Invariants are enforced in the service AND the schema — append-only ledger, zero-floor balances (a debit throws and a SQL trigger blocks it), expectation-pays-zero, household-scoped RLS. A rule that matters is defended in two places.
- SDK construction is config-driven —
createClient({required ClientConfig config}). You never inject an adapter;ClientConfig(api: ApiConfig(...))(public anon key only) switches local→cloud. The production local store is a write-through cache over Drift. - Presentation never imports
drift/supabase/ any I/O — only theclient_sdkfacade.client_sdk/src/is not exported; the barrel is the facade + models +createClient. - Brand lives only in
app/lib/inside/i18n+ store metadata — never in package/class/file names.
Inside vs Outside
The app layer splits singletons created before the widget tree (outside) from things created inside it.
OUTSIDE — built in the app runner/builder, passed into the tree:
| Layer | Role | Mocked in tests? |
|---|---|---|
| Client Providers | wrap external SDKs (Supabase, Sentry) | never |
| Effect Providers | create Effect instances (analytics, prefs, …) | yes |
| Repositories | the presentation seam over the SDK facade | yes |
INSIDE — created within the widget tree: Blocs/Cubits (never mocked),
Effects, Widgets/Pages (pages implement AutoRouteWrapper for scoped
bloc injection). Nothing inside the tree touches a Client Provider directly —
only Repositories and Effect Providers do.
The bloc state contract (load-bearing)
Every bloc state is @JsonSerializable() + Equatable, with a Status enum
and explicit toJson/fromJson (+ part 'state.g.dart'). This is what the
DevTools bloc plugin serializes — so it is non-negotiable. See
Add a bloc & page.
Routing & guards
Routes are auto_route (@RoutePage() + AppRouter), with
AuthenticatedGuard / UnauthenticatedGuard driving redirects from the auth
state. The unauthenticated landing resolves to the intro carousel (first
launch) or Welcome; a signed-in account with no household is routed to the
Setup wizard.
The packages
client_sdk— the data layer (facade/services/adapters/models). Pure Dart, nodart:ui.client_sdk_testing— the in-memory adapter, seed factories, andMockClient(the canonical SDK test doubles).design_system— tokens + theme + atoms. Model-agnostic — noclient_sdkdependency. One token source (DsTheme), dark default, split type (Bricolage display / Atkinson body), an a11y floor in every atom.flow_test— the UI flow-test harness (see Write a flow test).
Next: the Guides for the task recipes.