---
title: "Security and privacy"
url: https://phatdev.net/en/docs/moneyflow/bao-mat
lang: en
updated: 2026-10-23
alternate:
  vi: https://phatdev.net/docs/moneyflow/bao-mat
markdown: https://phatdev.net/en/docs/moneyflow/bao-mat.md
---

# Security and privacy

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

> App lock, data encryption, the sync server, demo mode and the limits to know about.

**MoneyFlow** handles the whole household's financial data, so security is designed in from the start: data is encrypted while the device is locked, the app holds no secrets, and the server is run by the household itself. This page says what the app protects, and is just as direct about the limits.

<div style="margin:1.6em 0"><figure style="margin:0"><a href="https://phatdev.net/uploads/p/moneyflow/security-vi-light-3840.webp" target="_blank" rel="noopener" class="pd-th-light" style="display:block;border:0" aria-label="Account: sync, Face ID lock, CSV export (sample data) (4K)"><img src="https://phatdev.net/uploads/p/moneyflow/security-vi-light-1920.webp" srcset="/uploads/p/moneyflow/security-vi-light-960.webp 960w, /uploads/p/moneyflow/security-vi-light-1920.webp 1920w, /uploads/p/moneyflow/security-vi-light-3840.webp 3840w" sizes="(max-width: 900px) 100vw, 760px" width="3840" height="2160" alt="Account: sync, Face ID lock, CSV export (sample data)" loading="lazy" decoding="async" style="width:100%;height:auto;display:block;border:1px solid var(--line);border-radius:12px"></a><a href="https://phatdev.net/uploads/p/moneyflow/security-vi-dark-3840.webp" target="_blank" rel="noopener" class="pd-th-dark" style="display:block;border:0" aria-label="Account: sync, Face ID lock, CSV export (sample data) (4K)"><img src="https://phatdev.net/uploads/p/moneyflow/security-vi-dark-1920.webp" srcset="/uploads/p/moneyflow/security-vi-dark-960.webp 960w, /uploads/p/moneyflow/security-vi-dark-1920.webp 1920w, /uploads/p/moneyflow/security-vi-dark-3840.webp 3840w" sizes="(max-width: 900px) 100vw, 760px" width="3840" height="2160" alt="Account: sync, Face ID lock, CSV export (sample data)" loading="lazy" decoding="async" style="width:100%;height:auto;display:block;border:1px solid var(--line);border-radius:12px"></a><figcaption style="margin-top:8px;font-size:13px;line-height:1.5;color:var(--muted);text-align:center">Account: sync, Face ID lock, CSV export (sample data)</figcaption></figure></div>

## App lock

Turn it on in Account, under Bảo mật (Security): "Khoá bằng Face ID" (lock with Face ID), or Touch ID depending on the device. The switch is available only when the device has Face ID, Touch ID or a screen passcode set up.

- The app locks every time it goes to the background and unlocks with biometrics; you can enter the device passcode if biometrics are unavailable.
- The lock screen sits in a separate window above **everything**, open sheets included.
- When the app is only briefly inactive (the app switcher, the notification shade), the screen is covered so the snapshot in the app switcher does not reveal figures.
- If the device no longer has biometrics or a passcode, the app does not lock you out of your own data.

## Data on the device

- Data is stored with SwiftData, and the store file has `NSFileProtectionComplete`: encrypted while the device is locked and readable only once it is unlocked.
- The sync session token is kept in the Keychain, readable only when the device is unlocked and only on this device. Signing out or an expired session deletes it.
- The app contains no passwords, password hashes or API keys. The gold price source's key lives on the server.
- If a store upgrade fails, the old store is renamed and kept instead of being deleted.

## The sync server

- Authentication happens entirely on the server, with passwords hashed with bcrypt and sessions as JWTs. Sign-in responds in uniform time, so it does not reveal which accounts exist.
- Failed sign-ins are rate limited by the caller's real IP address and by account name. The per-account limit is set generously so nobody can lock someone else out by failing logins on purpose.
- By default the server listens only on the loopback interface and sits behind a tunnel or an HTTPS reverse proxy. The app uses HTTPS, so transport security is not relaxed.
- It uses helmet for security headers, a locked-down CORS policy (only the iOS app calls it) and request size limits.
- The server refuses to start if `JWT_SECRET` is missing or still the example value. There is no fixed default password: `db:init` generates a random password for each new account and never overwrites an existing one.
- Erasing all data on the server is for Manager accounts only, and runs inside one database transaction.

## Safe CSV export

The CSV file guards against formula injection: text cells that start with `=`, `+`, `-`, `@` or a control character get a single quote in front, so Excel or Numbers do not run them as formulas.

## Demo mode never touches real data

Demo mode in Debug builds uses its own data store and made-up member names, sync is disabled and real bill notifications are untouched. A device that ran an older build's demo data inside the real store is blocked from syncing until its local data is cleared, so sample data cannot reach the server. Public screenshots use made-up data only.

## Limits to know about

- **One shared ledger is deliberate.** Every member with an account can see all of the household's transactions, so that splitting is accurate. MoneyFlow has no privacy between members.
- **Sync is not end-to-end encrypted.** The shared ledger sits on the household's server, protected by HTTPS in transit and by access to the server. Keep the server secure.
- **No automatic password recovery.** Whoever runs the server resets a password with the `npm run user:passwd` command.
- **Each device's personal data** (savings, gold, debts, bills, recurring items) is not backed up to the server. Use iOS backups for your device, and export CSV for transactions when you need to.
