MoneyFlow is an iOS/iPadOS app written in SwiftUI and SwiftData (Swift 5, targeting iOS and iPadOS 26.1 or later) plus a Node.js + Express + PostgreSQL sync server. The architecture centres on keeping a multi-person shared ledger consistent, even when a device is offline or its clock is wrong.
Code structure
MoneyFlow (iOS / iPadOS app)
├── Views SwiftUI screens: Overview, Transactions, Savings, Categories, Account...
├── Managers DataManager, SavingsManager, UserManager, AppLockManager, NotificationManager (@Observable)
├── Models SwiftData models: Transaction, Category, Budget, Gold, Savings, SavingsGoal, DebtRecord...
├── Services SyncManager (push/pull sync), GoldPriceService (gold price)
├── Helpers CategorySuggester, DataExporter (CSV), ImageStorageHelper, DemoData
└── Theme.swift Design system: colours, typography, gradient cards, Liquid Glass
Backend (Node.js + Express + PostgreSQL)
├── routes auth, sync, gold
├── middleware JWT authentication
└── db schema setup and connection pool
| Part | Role |
|---|---|
Views |
Five tabs: Overview, Transactions, Add (opens the entry screen), Savings, Categories. Budgets, Reports, Recurring, Bills, Debts and Family split live under Account. |
Managers |
Data layer using the @Observable macro: DataManager (transactions, categories, budgets, recurring items, bills, debts, family split), SavingsManager (gold, savings, goals), UserManager, AppLockManager, NotificationManager. |
Models |
Ten SwiftData models: Transaction, Category, Budget, User, Gold, Savings, RecurringTransaction, BillReminder, DebtRecord, SavingsGoal. |
Services |
SyncManager syncs with the server; GoldPriceService fetches the gold price. |
Helpers |
CategorySuggester (category suggestion), DataExporter (CSV), ImageStorageHelper (receipt photos), DemoData (Debug builds only). |
Theme.swift |
Adaptive light/dark colours, Dynamic Type typography, tabular figures for amounts, four gradient card styles (emerald, coral, gold, violet), and Liquid Glass for the navigation bar with a fallback. |
The server's main endpoints are POST /api/auth/login, GET /api/sync/pull, POST /api/sync/push, POST /api/sync/reset and GET /api/gold/price.
Sync rules
- Default categories have deterministic identifiers derived from name and type, so every device agrees without asking the server. Renaming a default category must keep its original identifier through
idSeed(for example "Xe cộ" was renamed "Độ xe" and kept its old identifier). - Deletes are soft deletes (
isDeleted) so they propagate to other devices. - Sync cursors use server time. Each server row has two timestamps:
updated_at, supplied by the device and used only for "latest edit wins" conflict resolution, andserver_updated_at, assigned by the database and used as the pull cursor. That way a late push from a skewed-clock device still reaches the others. - Pull does not skip rows. A push holds a shared advisory lock inside its transaction, while a pull takes the exclusive lock just long enough to open its snapshot and read the server clock. When the cursor is fixed, every push in flight has already committed.
- Pushed data is validated by a dedicated validator (
validatePushBody) with column length limits, records per table (5,000), number of tags, a valid year range, andupdated_atnot allowed to exceed the current time by more than 5 minutes. - Pushes go in batches of 500 records. If a batch fails, no progress is recorded and the next sync sends it again.
Data and security rules
- No passwords, API keys or real financial data in source; sensitive configuration lives in
.env, which is git-ignored. - Amounts are in dong (VND) and displayed in Vietnamese style:
1.234.567 đ, compact form51,9tr. A single hand-entered transaction is capped at 100 billion dong. - Demo data lives in its own store, and sync is disabled in demo mode.
- When the schema cannot be opened, the old store is renamed and kept rather than deleted.
- Bill notifications are rescheduled from scratch on every launch, and notifications of bills that no longer exist are swept away.
Testing
- 63 app tests (
MoneyFlowTests) run on an in-memory SwiftData store with the full schema. They cover bills (cycles, overdue), recurring transactions (original day, pause), rolling budgets, gold price parsing and the stale state, money formatting, CSV export, the transaction entry screen and the category suggester. - Server tests cover sync payload validation:
npm testrunsnode test/validate.test.js. - Run the app tests with the release Xcode; a beta can leave the test runner unresponsive.
xcodebuild test -project MoneyFlow.xcodeproj -scheme MoneyFlow \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-only-testing:MoneyFlowTests
Contributors
- Steven Lam: author. Product planning and direction, architecture, design direction, server operations, real-world testing and releases.
- Claude (Anthropic): writes code to the author's plan and architecture, via Claude Code. Commits written with Claude carry a
Co-Authored-By: Claudetrailer. - Gemini (Google): interface refinements, via Antigravity.
- Codex (OpenAI): code review and bug reports.
Every change is verified by running the real tests, not by relying on descriptions.