Skip to main content

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 BlocProvider in a page's wrappedRoute().
  • Repository — a thin presentation delegate. It adapts the SDK facade to what blocs need; it does not hold domain rules.
  • Client (facade) — the client_sdk public 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-drivencreateClient({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 the client_sdk facade. 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:

LayerRoleMocked in tests?
Client Providerswrap external SDKs (Supabase, Sentry)never
Effect Providerscreate Effect instances (analytics, prefs, …)yes
Repositoriesthe presentation seam over the SDK facadeyes

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, no dart:ui.
  • client_sdk_testing — the in-memory adapter, seed factories, and MockClient (the canonical SDK test doubles).
  • design_system — tokens + theme + atoms. Model-agnostic — no client_sdk dependency. 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.