---
title: "Architecture and engineering rules"
url: https://phatdev.net/en/docs/moneyflow/kien-truc
lang: en
updated: 2026-10-23
alternate:
  vi: https://phatdev.net/docs/moneyflow/kien-truc
markdown: https://phatdev.net/en/docs/moneyflow/kien-truc.md
---

# Architecture and engineering rules

Part of the doc set: [MoneyFlow](https://phatdev.net/en/docs/moneyflow.md)

> SwiftUI + SwiftData, the Node.js server, sync rules, payload validation, testing.

**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, and `server_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, and `updated_at` not 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 form `51,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 test` runs `node test/validate.test.js`.
- Run the app tests with the release Xcode; a beta can leave the test runner unresponsive.

```sh
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: Claude` trailer.
- **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.
