WeSpend - System Design
Overview
WeSpend is an offline-capable, real-time-synced household expense tracker built as an Expo / React Native app on a Supabase backend. The codebase follows a layered (clean-architecture-inspired) structure so the business rules are isolated from frameworks and easy to test.
Layers
app/ Expo Router routes (thin; delegate to presentation)
src/
presentation/ Screens and components (NativeWind). UI only.
application/ Use-cases: TanStack Query hooks + Zustand stores.
data/ Repositories, Supabase client, mappers, DB row types.
domain/ Pure business rules. No framework imports.
shared/ Cross-cutting config and utilities.Dependency rule: inner layers never import outer layers. domain imports nothing from data, application, or presentation. data maps DB rows to domain entities. application orchestrates data and domain. presentation consumes application.
flowchart TD
UI[presentation - screens/components] --> APP[application - hooks/stores]
APP --> DATA[data - repositories]
APP --> DOMAIN[domain - business rules]
DATA --> DOMAIN
DATA --> SB[(Supabase: Postgres + Realtime + Auth)]
DATA --> CACHE[(SQLite offline cache)]Why this shape
- The crown-jewel logic (budget limits, calendar-week pro-rating, rollover, settlement) is pure and lives in
domain, fully unit-tested with zero mocks. - Swapping or upgrading the backend touches only
data. A future web client reusesdomainandapplicationunchanged. presentationstays declarative and thin.
Data flow: logging an expense
- User submits the add-expense form (
presentation). useAddExpensemutation (application) callsaddExpense(data).- Repository inserts into Supabase; Realtime broadcasts the change.
- Every member's device receives the change and TanStack Query refetches/updates.
domainrecomputes daily/weekly status and settlement from the expense list.
Real-time sync
Supabase Realtime publishes row changes on budgets, expenses, settlements, members, categories, and the three bill tables (bill_settings, bill_categories, bill_entries). The application layer subscribes per household and invalidates the relevant TanStack Query keys, so all members see updates live. Offline edits are cached in SQLite and reconciled on reconnect (last-write-wins via updated_at).
Notifications
Beyond in-app realtime, WeSpend sends push notifications across devices (via Expo push tokens in device_push_tokens) for household activity, budget-threshold crossings (80% and over), and meal-card low balance. A persistent in-app feed lives in the notifications table. The weekly settlement reminder is server-driven: a pg_cron job POSTs through a Supabase Edge Function on a schedule, independent of any device being open. See Data model.
Known simplifications (MVP)
- Mid-month budget edits currently recompute a single frozen daily limit rather than segmenting old vs new days. The PRD's "future days only" behavior is a planned refinement (tracked as a follow-up issue).
- Offline SQLite reconciliation is last-write-wins; per-field merge is deferred.
- Anonymous-auth identity is device-bound; recovery on reinstall is a planned enhancement.