PFM — Personal Finance Manager
A self-hosted personal finance web application. Track income, expenses, investments, budgets, and savings goals. AI-powered financial assistant and receipt OCR built in. Sync bank accounts via Teller, Plaid, or Schwab.
Live at: https://pfm.ngodanguyen.tech
Table of Contents
- Features Overview
- Getting Started
- Dashboard
- Transactions
- Accounts
- Categories
- Budgets
- Goals & Savings
- Investments
- AI Assistant
- Reports & Export
- Settings
- Security & 2FA
- Recurring Transactions
- Bank Statement Import
- Receipt OCR
- Teller Bank Sync
- Schwab Bank Sync
- Plaid Bank Sync
- USD → VND Rate Widget
- System Logs
- Keyboard Shortcuts & Tips
Features Overview
| Module | What it does |
|---|---|
| Dashboard | Net worth, savings rate, cash flow chart, budget gauges, upcoming bills, reconcile, AI insight, USD/VND rate |
| Transactions | Income/expense entry, transfer, inline category, bulk actions, quick date filters, duplicate detection, split transaction, filtered export |
| Accounts | Multiple account types, auto-calculated or provider-synced balances (Teller / Plaid / Schwab) |
| Categories | Custom expense + income categories with color and icon |
| Budgets | Monthly spending limits with progress bars, budget vs actual chart, rollover support |
| Goals | Savings goals with contribution tracking, projected completion, emergency fund tracker |
| Investments | Stock/ETF/crypto portfolio, per-account view, price alerts, price history chart, live price fetch |
| AI Assistant | Chat with your finances via Groq (free), daily auto-insight, streaming responses |
| Receipt OCR | Drag-drop a receipt image — AI extracts and fills the form |
| Reports | Monthly/quarterly/yearly summaries, net worth history, category spending trends, PDF/CSV/Excel export |
| Teller | Live US bank sync (mTLS), balance refresh, transaction import, webhook |
| Schwab | Schwab brokerage sync — balance, stock/ETF positions, transaction import, daily auto-sync |
| Plaid | 12,000+ US banks/credit unions via Plaid — cursor sync, credit card billing, webhook auto-import |
| Bank Import | Upload PDF/OFX/QFX/CSV bank statements — auto-categorizes, preview before import |
| Security | TOTP 2FA, session idle timeout, rate limiting, audit log, at-rest encryption for bank tokens |
| System Logs | DB-backed application log viewer with colour-coded levels, search, module filter, and purge |
Getting Started
First Login
- Navigate to
https://pfm.ngodanguyen.tech - Enter the username and password created during setup (
python scripts/init_db.py) - Check "Remember me" to stay logged in across browser sessions
Recommended Setup Order
- Add accounts — add your bank accounts, cash wallet, and credit cards first
- Review categories — 21 default categories are seeded; add custom ones if needed
- Set your currency — go to Settings → Profile and pick from 8 currency options
- Connect bank sync — connect Teller (US banks), Plaid (12,000+ institutions), or Schwab for automatic data
- Add transactions — enter income/expenses manually, import a bank statement, or sync from a provider
- Set budgets — once you have spending data, set monthly limits per category
- Create goals — add savings goals and start contributing
- Add investments — manually or via Schwab sync
- Enable 2FA — recommended: Settings → Security → Two-Factor Authentication
Dashboard
The dashboard is the home screen — accessible from the sidebar or by clicking the PFM logo.
Period Selector
Three buttons in the top-right: This Month, Last Month, Custom (date range picker). All summary cards update for the chosen period.
Summary Cards
- Income — total income for the selected period
- Expenses — total expenses for the selected period
- Net Cash Flow — income minus expenses
- Savings Rate — net cash flow ÷ income (green ≥ 20%, blue > 0%, red negative)
- Net Worth — total assets minus liabilities
- Checking & Savings — sum of checking, savings, and cash account balances
- Investments — sum of investment and crypto account balances
Reconcile Button
Click Reconcile (next to period selector) to recalculate income, expenses, net cash flow, and savings rate while excluding internal transfers. This shows your true external cash flow. A notice shows the excluded transfer amounts and which categories were excluded. Click again to toggle back to the original totals.
Cash Flow Chart
Bar chart showing the last 6 months of income (green) vs expenses (red).
Upcoming Bills
Table of recurring rules due within the next 14 days — category icon, rule name, due date (color-coded: today/overdue in red, within 3 days in amber), amount, and account. Links to the Recurring page.
Accounts Panel
Lists all active accounts with current balances. Credit cards show Amount Owed (positive) instead of a raw negative balance.
AI Daily Insight
Auto-generated summary of your finances at midnight. Click Generate Now for an on-demand insight. Click Open AI → for the full chat interface.
USD → VND Widget
Reference-only exchange rate. Click ↻ to refresh without reloading. Click the widget to expand a 30-day trend chart. A ⚠ stale indicator appears if the rate is older than one day.
Schwab Expiry Warning
A banner appears on the dashboard when your Schwab refresh token expires within 2 days. Click the link to reconnect.
Transactions
Viewing Transactions
Navigate via Transactions in the sidebar. Two tabs: Expenses and Income (with total counts).
Filtering
- Search — matches description and notes fields
- Category dropdown
- Account dropdown
- Date range — from/to pickers
- Amount range — min/max amount
- Quick date filters — "This Month" and "Last Month" buttons above the filter bar; active button is highlighted; ✕ clears the quick filter
- Saved filter presets — save the current filter combination under a name; reload it from the dropdown in one click
Plaid Review Banner
When transactions are imported via Plaid webhook without a category, a purple banner at the top of the Transactions page shows the count and a link to review and categorize them. The sidebar Transactions link also shows a badge with the count.
Adding a Transaction
Use the topbar Income / Expense buttons, or use the sidebar links. The form includes:
- Type toggle (Income / Expense) — switches available categories
- Description, amount, date, account, category, notes
- Duplicate detection — if the same amount on the same date already exists, a yellow warning banner appears before you save
Inline Category Change
Click the category dropdown on any transaction row in the table to change the category. The change saves via AJAX — no page reload.
Editing and Deleting
Click Edit to open the full form. Click Del to delete permanently.
Split Transaction
Click the ✂ (scissors) icon on any transaction row to split it into multiple parts:
- A split page shows the original transaction details and two default rows
- Assign a different category, description, and amount to each part
- A live Remaining counter shows how much is left to allocate
- Add or remove rows as needed; the total must equal the original amount
- On confirm, the original transaction is replaced by the individual split transactions
Bulk Actions
Check one or more transaction rows (or use the select all header checkbox) to reveal the bulk action toolbar:
- Set category — apply a category to all selected rows at once; dropdowns update in the table without reload
- Delete — delete all selected transactions after a confirmation dialog
Export Filtered View
In the filter bar, CSV and Excel buttons export the current filtered view (respects all active filters — search, category, account, date range, amount range). Exports are streamed for memory efficiency — no row limit.
Transfers
Click Transfer in the topbar. Creates a single transfer record between two accounts. Transfers are excluded from income/expense totals.
Accounts
Account Types
checking / savings / cash / credit_card / crypto / investment / other
Balance Sources
- Manually managed — balance is computed from transactions (income − expenses ± transfers). Updates after every transaction.
- Teller-linked — live balance from Teller API. Blue Teller badge. Updated after sync or Refresh click.
- Schwab-linked — balance from Schwab snapshot (liquidation value). Green Schwab badge. Updated after Balance & Positions click or daily auto-sync.
- Plaid-linked — live balance from Plaid API. Purple Plaid badge. Updated after sync or Refresh click.
Teller Account Actions
| Button | What it does |
|---|---|
| Refresh | Pulls live balance from Teller API (AJAX) |
| Sync | Opens transaction preview — import new transactions |
| Reset | Clears sync cursor — next Sync re-fetches full 90-day history |
Schwab Account Actions
| Button | What it does |
|---|---|
| Balance & Positions | Fetches live balance + investment holdings from Schwab |
| Transactions | Opens transaction import preview |
Plaid Account Actions
| Button | What it does |
|---|---|
| Refresh | Pulls live balance from Plaid API (AJAX) |
| Sync | Opens transaction preview — import new transactions |
| Billing | Fetches credit card due date, minimum payment, and statement balance |
Plaid Credit Card Billing Card
For Plaid-linked credit cards, a billing card on the account shows: due date, days remaining, minimum payment, and last statement balance. Updated by clicking Billing.
Credit Cards
Credit cards show two values:
- Amount Owed — how much you currently owe (positive number)
- This Month — expenses charged to the card this calendar month
Opening Balance
When creating a new account, enter the current balance in the Opening Balance field. For credit cards, enter a negative number to indicate existing debt (e.g. -500 if you owe $500).
Categories
Navigate via the sidebar footer → Categories.
Default Expense (14): Housing, Food & Dining, Transport, Utilities, Health, Entertainment, Shopping, Education, Insurance, Personal Care, Travel, Subscriptions, Gifts, Other
Default Income (7): Salary, Freelance, Business, Investment, Rental, Gift Received, Other Income
System categories (grey badge) cannot be deleted — color and icon can still be changed. Custom categories can be fully edited and deleted.
Budgets
Navigate via Budgets in the sidebar. Use ◀ ▶ to navigate months.
Setting Budgets
Click Add Budget in the topbar to set a monthly limit for a category. Click Edit on an existing row to adjust.
Budget vs Actual Chart
A horizontal bar chart at the top of the page shows Spent (color-coded green/amber/red) vs Budget limit per category — for all categories that have a budget set this month.
Progress Bars
Each row shows a progress bar: green (< 80%) → amber (80–99%) → red (≥ 100%). Rows that exceed the limit have a red background tint.
Rollover
Enable rollover on a budget to carry unused amounts forward to the next month. A blue +rollover badge shows the carried-over amount on the row.
Copy from Previous Month
Click Copy from [month] at the top right to duplicate last month's budgets to the current month in one click. Also available on the empty-state screen.
Goals & Savings
Navigate via Goals in the sidebar.
- Create goals with target amount, target date, color, and icon
- Add contributions with dates and notes; view contribution history
- Progress bar + projected completion date (based on average monthly contribution)
- Auto-completes when 100% is reached
- Emergency Fund Tracker — shows how many months your liquid assets (checking + savings + cash accounts) cover at your current monthly spending rate, with 3-month and 6-month targets
Investments
Navigate via Investments in the sidebar.
Portfolio Overview
Summary cards: Total Value, Total Cost, Unrealized P&L, Return %. Allocation doughnut chart by asset type. Sidebar "By Account" breakdown.
Price Alerts
A yellow banner at the top of the Investments page appears when any holding moves ≥ 5% intraday. The banner lists each mover with its ticker and change percentage. Dismiss with ×. Alerts are recalculated daily when prices update.
Investment Detail Page
Click any holding to see the detail page:
- Current price, day change, P&L
- Price history chart — 1W / 1M / 3M / 6M / 1Y timeframes; fetched from Yahoo Finance
- Full buy/sell/dividend/split transaction log
Per-Account View
If investments span multiple accounts (e.g. Schwab Individual + Schwab Roth IRA), holdings are grouped into separate sections — one per account. Each section shows the account name, total value, and its own holdings table. Same ticker in different accounts stays as separate rows.
Syncing from Schwab
Click Sync Schwab in the topbar to pull the latest holdings and balances for all mapped Schwab accounts at once.
Adding a Holding Manually
Click Add Holding in the topbar. After saving, go to the detail page to record buy transactions.
Ticker Format
- US Stocks/ETFs:
AAPL,VOO,QQQ - Crypto:
BTC-USD,ETH-USD - International: Yahoo Finance suffix (e.g.
VIC.VN)
Refreshing Prices
Click Refresh Prices in the topbar. Prices also auto-update daily at 4 PM weekdays via cron.
AI Assistant
Navigate via AI Assistant in the sidebar.
The AI has access to the last 90 days of transactions, budget status, goals, investments, and net worth. No personal names or identifiers are sent — only aggregated financial figures.
- Type a message and press Enter to send; Shift+Enter for a new line
- Responses stream word-by-word (SSE)
- Eight quick-question suggestion buttons
- Daily Insight — auto-generated at midnight; click Generate Now to create on demand
- Chat History — all past responses with timestamps
AI Model
Change in Settings → Profile → AI Model:
llama-3.3-70b-versatile— best quality (default)llama-3.1-8b-instant— faster, slightly lower quality
Reports & Export
Navigate via Reports in the sidebar.
Four tabs: Monthly, Quarterly, Yearly, Tax Year.
Export buttons in the topbar:
- CSV — plain text, streamed (no row limit)
- Excel — color-coded (green = income, red = expense), formatted, streamed
- PDF — printable report via WeasyPrint
Net Worth History — line chart from monthly snapshots. Snapshot Now saves today's values immediately.
Category Spending Trends — top 6 categories over the last 6 months.
Settings
Navigate via the gear icon at the bottom of the sidebar.
| Section | What you can change |
|---|---|
| Profile | Display name, email, timezone, currency (8 options), AI model |
| Password | Change password (requires current password) |
| Security | Enable/disable TOTP 2FA; view Audit Log |
| Recurring | Create/edit/pause recurring transaction rules |
| Import | Upload CSV or bank statement for bulk import |
| System Logs | View, search, and purge application logs |
All Settings sub-pages have a ← Settings back button in the topbar.
Security & 2FA
Two-Factor Authentication (TOTP)
- Go to Settings → Security section
- Click Enable Two-Factor Authentication
- Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.) or enter the manual key
- Enter the 6-digit code to confirm setup
- On future logins, you will be prompted for a 6-digit code after password entry
To disable 2FA: Settings → Security → Disable (requires password confirmation).
The TOTP verify endpoint is rate-limited (10/min, 30/hr). After 5 failed attempts in one session, the login is invalidated and you must start over.
Session Idle Timeout
Sessions expire after 60 minutes of inactivity (configurable via SESSION_IDLE_MINUTES in .env).
Audit Log
Settings → Audit Log shows a paginated, filterable log of security events with IP addresses and timestamps:
- Login success / failure (with and without 2FA)
- 2FA enabled / disabled
- Password changed
- Schwab connected / disconnected
Filter by event type. Purge dropdown removes entries older than 7, 30, or 90 days.
At-Rest Encryption
Teller, Schwab, and Plaid OAuth tokens are encrypted in the database using AES-128 (Fernet). Changing SECRET_KEY in .env invalidates stored tokens — all providers would need to reconnect.
Recurring Transactions
Navigate via Settings → Recurring.
Rules auto-create transactions on a schedule. Frequencies: daily, weekly, biweekly, monthly, quarterly, yearly.
- Set start date, optional end date, account, category, and amount
- Pause/enable individual rules without deleting them
- Run Now button processes all overdue rules immediately
- Upcoming (30 days) panel shows next-due dates for all active rules
- Rules also appear on the Dashboard upcoming bills widget when due within 14 days
Bank Statement Import
Navigate via Settings → Import Statement (sidebar) or Settings → Import.
Supported Formats
| Format | Notes |
|---|---|
| Digital (text-based) PDFs only — scanned/image PDFs are rejected. Uses table extraction first; falls back to AI parsing via Groq if no structured table is found | |
| OFX / QFX | Standard Open Financial Exchange format (most US banks) |
| CSV | Chase, BofA, Citi, Capital One, Discover, Amex, USAA, Wells Fargo, and Generic CSV |
| Custom CSV | Map your own column headers when the bank format isn't recognized |
Import Flow
- Upload the file
- Preview table — per-row checkboxes, editable category dropdowns, bulk category apply, bulk type toggle
- Click Confirm Import to write to the database
Duplicate Detection
- OFX/QFX: uses the
FITIDfield - CSV/PDF: matches on date + amount + description + account
Auto-Categorization
200+ keyword rules automatically assign categories to imported transactions. Override any row in the preview before confirming.
Receipt OCR
On the new expense form: drag-drop or click the purple AI Receipt Scanner panel to upload a receipt image (JPG/PNG/GIF/WEBP, max 10MB). The AI extracts amount, date, merchant name, and category suggestion, then fills the form fields with a green flash animation. Always review before saving.
On the edit form:
- Re-extract button re-runs OCR on the already-attached receipt
- OCR also runs automatically when you select a new image file in the upload field
Teller Bank Sync
Teller connects US bank accounts using a secure mTLS connection. Accessible via Settings → Bank Connections.
Connecting
- Go to Settings → Bank Connections
- Click Connect a Bank — the Teller Connect modal opens
- Select your bank and log in
- After connecting, use Map Account on any unmapped accounts to link them to PFM accounts
Syncing Transactions
On the Accounts page, find a Teller-linked account (blue Teller badge) and click Sync. Review the transaction preview and confirm to import. After import, the live balance is fetched from Teller automatically.
Webhook
Teller can push transaction updates to PFM automatically. The webhook endpoint verifies the Teller-Signature header (HMAC-SHA256) and has a 5-minute replay window.
Disconnecting
Settings → Bank Connections → Disconnect. Imported transactions are kept.
Schwab Bank Sync
Schwab integration uses OAuth 2.0 to sync brokerage and IRA accounts. Accessible via Settings.
Connecting
- Set
SCHWAB_CLIENT_ID,SCHWAB_CLIENT_SECRET,SCHWAB_REDIRECT_URIin.env - Register the redirect URI in the Schwab developer portal (exact match required)
- Settings → Schwab → Connect Schwab Account
- Authorize on Schwab's login page
- On the Map Accounts page, link each Schwab account to a PFM account (or create new ones)
Syncing Balance & Investment Positions
On the Accounts page, click Balance & Positions on a Schwab account (green badge). This:
- Updates the account balance to Schwab's total portfolio liquidation value
- Upserts stock/ETF/bond/fund holdings into the Investments section
- Associates each holding with the specific account (Individual vs Roth IRA stay separate)
Also click Sync Schwab in the Investments page topbar to update all Schwab accounts at once.
Syncing Transactions
Click Transactions on a Schwab account card. Review the preview and confirm to import.
Token Expiry
Schwab access tokens expire after 30 minutes (auto-refreshed). Refresh tokens last 7 days — a dashboard warning appears when ≤ 2 days remain. Reconnect via Settings → Schwab to reset the expiry.
Daily Auto-Sync
A cron job runs at 7 AM daily (scripts/sync_schwab.py) — updates balances, positions, and transactions for all mapped Schwab accounts automatically.
Disconnecting
Settings → Schwab → Disconnect. Imported transactions and holdings are kept.
Plaid Bank Sync
Plaid connects to 12,000+ US banks and credit unions. Accessible via Settings.
Connecting
- Set
PLAID_CLIENT_ID,PLAID_SECRET,PLAID_ENV(sandboxorproduction) in.env - Settings → Plaid → Connect Bank Account
- The Plaid Link widget opens — search for your institution and log in
- On the Map Accounts page, link each Plaid account to a PFM account (or create new ones)
Syncing Transactions
On the Accounts page, click Sync on a Plaid-linked account (purple badge). This uses cursor-based sync (/transactions/sync) — only new transactions since the last sync are fetched. Review the preview and confirm.
Reset Sync — click Reset to clear the sync cursor. The next sync re-fetches the full available history; duplicates are automatically skipped via the Plaid:<transaction_id> marker in notes.
Live Balance Refresh
Click Refresh on a Plaid account card to fetch the latest balance from Plaid's /accounts/balance/get endpoint.
Credit Card Billing
For Plaid-linked credit cards, click Billing on the account card to fetch:
- Next payment due date
- Minimum payment amount
- Last statement balance
- Overdue indicator
This information also appears in the billing card on the Accounts page.
Webhook Auto-Import
Plaid can push new transactions to PFM via webhook (POST /plaid/webhook). When a TRANSACTIONS/SYNC event arrives, new transactions are imported silently (no preview step). Transactions imported this way without a category appear in the Plaid review banner on the Transactions page and the sidebar badge — review and categorize them from there.
The webhook is verified using Plaid's JWT signature (ES256). Configure the webhook URL in Settings → Plaid → Apply to Existing Items.
Disconnecting
Settings → Plaid → Disconnect on a connection. Imported transactions are kept.
USD → VND Rate Widget
Reference-only exchange rate on the dashboard. Does not affect any app calculations.
- Click ↻ to force-refresh without reloading the page
- Click the widget to expand a 30-day trend chart
- Source label shown (yfinance / exchangerate-api)
- ⚠ stale indicator appears if the cached rate is more than one day old
System Logs
Navigate via Settings → System Logs (or sidebar → Settings → System Logs card).
Application log entries are stored in the database and shown in a colour-coded viewer:
- Filter by level (INFO / WARNING / ERROR / DEBUG) or free-text search
- Filter by module name (e.g.
app.teller,app.plaid) - Auto-refresh toggle for live monitoring
- Download — downloads the raw log file (
app.log) - Purge dropdown — delete DB entries older than 7, 30, or 90 days
- Clear All — truncates both the DB log table and the log file
- DB entry count shown in the page header
Keyboard Shortcuts & Tips
Navigation
- Sidebar collapses on desktop — click ☰ to toggle (state saved in localStorage)
- On mobile, tap outside the sidebar to close it; tables scroll horizontally
Transactions
- Enter sends in AI chat; Shift+Enter adds a new line
- Change category directly in the transactions table — no form needed
- Date defaults to today — change it for past transactions
- Use the ✂ Split button on a row to divide a transaction across multiple categories
- Export the current filtered view with the CSV / Excel buttons in the filter bar
Budgets
- Copy from previous month at the start of each month saves re-entering all budgets
- The Budget vs Actual chart gives a quick visual of where you stand each month
- Rollover amounts appear as a blue +rollover badge on the row
Investments
- Click any holding row to go to the detail page with P&L and price history chart
- ↓ button on the new transaction price field fetches the live price for that ticker
- Ticker symbols are auto-uppercased on save
- Price alerts banner appears automatically when any holding moves ≥ 5% intraday
Reports
- Snapshot Now saves today's net worth to the history chart immediately
- CSV and Excel exports from the Reports page use the selected period
- From the Transactions page, use the filter bar export buttons to export any filtered view
Security
- Enable 2FA in Settings → Security for additional login protection
- The Audit Log (Settings → Security → Audit Log) records every login attempt and security event with IP address
- Sessions time out after 60 minutes of inactivity