MSaving

Requirements and tests

Each requirement and the backend test that proves it.

Every requirement for the app, with the test in msaving/backend/tests/ that proves it. A new requirement gets a row here (in a docs PR) and a test (in the code PR). Run the tests: Tests. They need a superuser on a throwaway local Postgres 17, never a database you care about.

ID Requirement Test
Accounts and money
R-ACC-1 Users have several accounts: cash, checking, savings, investment, credit card, loan, money lent, other asset, other liability test_balances_and_ledger_rules, test_lending_and_borrowing, test_people_and_lending
R-ACC-2 Balances and net worth come from transaction history, never typed in test_balances_and_ledger_rules
R-ACC-4 An account can be left out of net worth; it still shows with its balance and the flag, and its spending still counts (from the old app) test_features_from_the_old_app
R-ACC-5 A new account can start with an opening balance (negative for what’s owed); retrying the request doesn’t add it twice test_accounts_with_opening_balances
R-ACC-3 A book has a currency chosen at setup; new accounts use it unless another is picked; an account’s currency is locked once used; changing the book’s currency converts its budgets test_account_currency_defaults_and_locks, test_exchange_rates
Transactions
R-TXN-1 Transactions are spending, income or transfers between accounts (plus refunds, opening balances, adjustments) test_balances_and_ledger_rules, test_dashboard_and_budgets
R-TXN-2 A transfer can’t create or destroy money; an expense is money out test_balances_and_ledger_rules
R-TXN-3 Each transaction can have a user-made category and an optional note test_balances_and_ledger_rules, test_dashboard_and_budgets, test_csv_export
R-CAT-1 Categories can be archived: hidden, history kept, their budgets stop showing test_archived_categories, test_categories
R-CAT-2 A category can have a colour (#rrggbb), exported with it (from the old app) test_features_from_the_old_app
R-CAT-3 A new book can start with starter categories and sub-categories in the user’s language, only in a book the user can edit (from the old app) test_features_from_the_old_app
R-CAT-4 Transfers can carry a transfer category, and only transfers can; it never counts as spending or takes a budget (from the old app, owner’s choice) test_features_from_the_old_app, test_categories
R-CAT-5 Categories form a tree that can’t loop; deleting one with transactions archives it instead and says so; one with sub-categories can’t be deleted test_categories
R-TXN-4 Transactions can be edited, with full edit history and a modified date test_edit_history_and_conflicts, test_transactions_and_two_browsers
R-TXN-5 The transaction list filters by account (with a running balance), category (sub-categories included), date range, type and text in payee or note, newest first, a page at a time test_transactions_and_two_browsers
Books and users
R-BOOK-1 A user can have several books, each a separate set of accounts test_books_are_isolated, test_books
R-BOOK-2 Books are separate: no transfers between books, and so none between users test_books_are_isolated, test_sharing_roles
R-USER-1 Every user has a unique username test_usernames_are_unique_and_lowercase
R-SHARE-1 A book can have several users, added by username, with owner/editor/viewer roles; nobody becomes a member without accepting test_sharing_roles, test_shared_books
R-SHARE-2 Nobody sees a book they aren’t an accepted member of, including a creator who has left; deleting a book deletes everything in it test_sharing_roles, test_audit_log_is_owner_only, test_shared_books
R-SHARE-3 Two users keep one book together; a viewer reads it and can’t change it; the last owner can’t leave or step down test_shared_books
R-SHARE-4 Inviting by username is rate limited, and an unknown username and an existing member get the same answer test_book_invites_are_rate_limited, test_shared_books
Several devices
R-DEV-1 Signed in on several devices: no lost edits, no duplicates from retries, balances stay right test_edit_history_and_conflicts, test_parallel_adds_all_count, test_pooled_connection_does_not_leak_user, test_books, test_accounts_with_opening_balances, test_transactions_and_two_browsers
People
R-LEND-1 Loans and money lent are tied to a named person, with who owes whom test_lending_and_borrowing, test_people_and_lending
Exchange rates
R-FX-1 Rates live in their own table and update automatically for everyone, free, from a second no-key source if the first fails test_fx_updater_job, test_exchange_rates, test_dashboard_converts_at_todays_rate, test_fx_sources_and_fallback
R-FX-2 Users can set their own rate, applying only to that book test_exchange_rates
R-FX-3 Every currency in use (ISO 4217) can be a book’s or an account’s currency, with the right number of decimals test_full_currency_list
R-FX-4 A book’s net worth includes accounts in other currencies at that day’s rate; accounts with no rate are left out and counted test_dashboard_converts_at_todays_rate
Dashboard and budgets
R-DASH-1 The home screen is a spending overview test_dashboard_and_budgets
R-BUD-1 Budgets per category: daily, weekly, monthly, quarterly, yearly test_budget_periods, test_dashboard_and_budgets, test_budgets
R-BUD-2 A budget counts its category’s sub-categories, one per category and period, only on expense categories; the dashboard shows each one’s progress test_budgets
Your data
R-EXP-1 Users can download all their data as CSV files test_csv_export, test_profile_and_export
R-EXP-2 The export is readable, not a copy of the database tables (no internal ids) test_csv_export
R-DEL-1 Users can delete their account, restore it within 30 days, then it’s deleted for good with its password, two-factor secret, recovery codes and sessions (and again after a backup restore) test_account_deletion_restore_and_purge, test_purge_removes_sign_in, test_close_and_restore
R-DEL-2 In shared books, a deleted user shows as “Deleted user”, each keeping their own id; their username can be taken by a new account with a new id test_account_deletion_restore_and_purge
R-DEL-4 Every account closure and restore is logged outside the database (ids and times only), after it’s committed test_close_and_restore
R-LANG-1 Vietnamese first, English second: each user has a language (Vietnamese by default) and the export follows it test_csv_export
Getting in and signing in
R-AUTH-1 A new server’s first account needs the setup code from the server’s log and becomes the admin; only once test_setup_creates_the_admin_once, test_migrate_builds_a_server_and_is_safe_to_repeat
R-AUTH-2 Sign-up is by invite only unless the admin opens it; invites work once, expire and can be revoked; the database refuses an account with no way in test_invite_only_server_needs_an_invite, test_revoked_and_expired_invites, test_admin_can_open_sign_up, test_temp_tables_cannot_shadow_real_ones
R-AUTH-3 Sign-in by username and password (10+ characters, argon2id); a wrong username and a wrong password look the same test_sign_in_and_wrong_credentials, test_sign_up_rejects_bad_input, test_username_taken
R-AUTH-4 Sessions are stored only as hashes, end after 30 days unused, on sign-out, sign-out-others and a password change test_tokens_are_stored_hashed, test_expired_sessions_are_refused, test_sign_out_and_sign_out_others, test_change_password_signs_out_other_devices, test_bad_tokens_are_refused
R-AUTH-5 Optional two-factor sign-in with one-time recovery codes; turning it off needs a current code test_two_factor_with_recovery_codes
R-AUTH-6 No email: the admin’s one-time reset link (or the operator’s command) sets a new password, turns two-factor off and ends every session test_admin_disables_and_resets
R-AUTH-7 Only the admin uses the admin tools; disabling an account signs it out at once; the admin can’t disable themselves test_only_the_admin_has_admin_tools, test_admin_disables_and_resets
R-AUTH-8 Sign-in, sign-up, setup, resets, codes and username changes are rate limited test_rate_limits, test_username_changes_are_rate_limited
Running an instance
R-INST-1 A server describes itself without sign-in (name, versions, sign-up mode, setup needed) test_instance_needs_no_token
R-INST-2 The server builds its database and logins on start, safe to repeat, with a backup before applying new migrations to an existing one test_migrate_builds_a_server_and_is_safe_to_repeat, test_launcher_builds_login_urls
R-INST-3 The server serves the web app with a strict Content-Security-Policy; unknown API paths don’t fall through to it test_web_app_files_and_routes, test_unknown_api_paths_are_404_not_the_web_app
R-INST-4 Daily jobs run at the configured hour test_scheduler_runs_daily_at_the_hour
R-INST-5 The admin sees the newest release; one-click update only through the operator’s updater helper test_update_info_and_one_click_update, test_versions_compare
R-INST-6 A nightly backup restores the app with its data, grants, policies and migration list, with the documented commands, and never shows the database password test_backup_restores_everything, test_restore_commands_documented, test_password_goes_to_pg_dump_only_as_pgpassword, test_errors_never_show_the_password, test_old_dumps_are_deleted_after_a_good_one, test_failed_backup_leaves_nothing_and_hides_the_password
R-INST-7 Timezone aliases a browser reports become names the database knows; unknown names are refused test_timezone_aliases_become_names_the_database_knows
Security
R-SEC-1 The database itself refuses access outside your books, even if app code has a bug every test that uses a second user
R-SEC-2 Background jobs (rates, deletion) can only do their one job; no login is superuser or bypasses RLS test_fx_updater_job, test_account_deletion_restore_and_purge, test_migrate_builds_a_server_and_is_safe_to_repeat
R-SEC-3 Supabase’s browser roles (on an operator’s own Supabase) can’t reach any table or function, including ones later migrations add test_supabase_api_roles_have_no_access
R-SEC-4 A session’s own temporary tables can’t stand in for real ones inside the database’s checks test_temp_tables_cannot_shadow_real_ones
R-SEC-5 Who created, edited or invited something, and who is the server’s admin, is set by the database; the app can’t forge it test_sharing_roles, test_nobody_can_make_themselves_admin
R-SEC-6 In a browser only the server’s own web app can use the API; phones (no Origin) connect freely; no CORS test_only_this_servers_web_app_and_any_phone, test_no_cors_by_default
R-SEC-7 One user’s profile and sign-in data are never visible to another test_me_returns_only_my_profile, test_sharing_roles

Not covered by these tests: the live exchange-rate call, HTTPS through Caddy, Tailscale or Cloudflare Tunnel, and the updater’s Docker commands (checked by hand before a release; see Releases). The web app’s string files are checked by its own tests.

Edit this page · Report a problem