Skip to content

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.

mermaid
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 reuses domain and application unchanged.
  • presentation stays declarative and thin.

Data flow: logging an expense

  1. User submits the add-expense form (presentation).
  2. useAddExpense mutation (application) calls addExpense (data).
  3. Repository inserts into Supabase; Realtime broadcasts the change.
  4. Every member's device receives the change and TanStack Query refetches/updates.
  5. domain recomputes 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.

Spend together, settle simply.