On this page
Project Overview
Tracking personal expenses manually can quickly become a tedious task. Every transaction needs to be recorded, categorized, and reviewed. Over time, this makes personal finance management difficult to maintain consistently.
I wanted to solve this problem by building a system that automatically records my financial activities without requiring manual transaction input.
The idea is simple: whenever I make a payment through myBCA, the bank sends a transaction notification to my email. Instead of treating these emails as simple notifications, I use them as a data source for my financial records.
The system automatically:
- Receives transaction notifications from Gmail using the Gmail API.
- Extracts and processes transaction details from the original email.
- Identifies the transaction type, amount, merchant, and other relevant information.
- Stores the processed data in a structured financial ledger.
- Generates financial summaries and spending reports through a web dashboard.
The application is designed to run locally, keeping sensitive financial records and authentication credentials within my own environment.
The main engineering challenges were ensuring secure email access, preserving original transaction data, and accurately handling different types of financial activities.
System Architecture & Data Flow
The application consists of two main components: a Go backend that handles email ingestion, transaction parsing, financial calculations, data processing, and database operations; and a Next.js frontend that provides a dashboard for reviewing transactions, managing classifications, and analyzing financial activities.
The overall data flow is designed as a one-way pipeline:
- myBCA Transaction → Gmail Notification → Gmail API (OAuth 2.0 + PKCE) → Go Backend → Raw Email Storage (PostgreSQL) → Transaction Parser → Transaction Classification → Financial Ledger → Authenticated Local API → Next.js Dashboard.
Each stage has a specific responsibility, making the system easier to maintain, test, and update independently.
Since the application handles sensitive banking information, email access needs to be restricted as much as possible. The backend uses Google's OAuth 2.0 Desktop Flow with PKCE to authenticate with Gmail. Refresh tokens are stored locally in a dedicated file with restricted file permissions (0600).
I also implemented a custom HTTP transport guard using Go's http.RoundTripper. Instead of relying only on application logic to avoid modifying emails, this component enforces read-only access at the HTTP transport level: only HTTP GET requests are allowed, requests are restricted to Gmail message listing and retrieval endpoints, redirects are rejected, and operations such as sending, deleting, moving to trash, or creating email drafts are blocked.
Bank notification formats can change over time. If transaction data is stored only after parsing, an update to the parser could make previously processed transactions difficult to recover. To prevent this, the backend first retrieves the original email in RFC 822 MIME format and stores its raw contents in PostgreSQL with a unique message identifier. Transaction parsing happens in a separate processing stage, ensuring the original email acts as a permanent source of truth for historical ledger replaying.

Key Engineering Challenges
Financial applications cannot rely on ordinary floating-point calculations because small rounding errors can accumulate over time. For example, using JavaScript's native Number type can introduce precision issues. To avoid this, I implemented exact decimal handling across the entire application:
- Transaction amounts are parsed directly from bank notification strings into exact decimal representations.
- PostgreSQL stores monetary values using strict decimal precision and scale constraints.
- Financial values are transferred through the API as strings to prevent precision loss.
- The frontend formats monetary values without converting them into floating-point numbers.
Not every transaction that moves money out of a bank account is an actual expense. For example, withdrawing Rp100,000 from an ATM simply moves money from a bank account to physical cash. If the system records this as an expense, it will incorrectly increase spending statistics and double-count money when later spent.
To prevent this, I implemented an asset registry classifying every transaction into one of four categories:
- Expense: Money spent on goods, services, or external parties.
- Income: Money received from an external source.
- Transfer: Money moved between accounts or assets owned by the user.
- Fee: Additional charges generated by financial transactions.
Bank transfers often include additional administrative fees (such as a Rp2,500 BI-FAST fee on a Rp100,000 transfer). A dedicated fee extraction component analyzes descriptions to separate transactions into two linked records: the primary transaction (Rp100,000) and the derived fee (Rp2,500), keeping expense categories clean while tracking fees accurately.
A financial ledger is only useful if its calculated balances match actual bank balances. I implemented a balance reconciliation engine where users periodically record actual account balances. The backend computes the expected balance (Previous Balance + Income - Expenses - Fees) and compares it with actual balances, generating a discrepancy value to identify missing transactions or parsing edge cases.
Implemented Features & Technology Stack
Beyond the core ingestion pipeline, the application includes several additional capabilities:
- Transaction Parsing: MIME email decoding supporting myBCA QRIS, BI-FAST, internal transfers, bill payments, ATM withdrawals, and PayPal notifications.
- Merchant Recognition & Normalization: Automatically identifies merchants from descriptions, supports configurable aliases, and groups name variations.
- Recurring Payment Detection: Analyzes transaction history to detect weekly, monthly, and annual intervals for subscriptions and bills.
- Personal Finance Dashboard: Built with Next.js 16 and React 19, providing transaction history, financial summaries, dark/light themes, keyboard-friendly classification, and consistent numerical formatting.
| Component | Technology |
|---|---|
| Backend | Go |
| Frontend | Next.js 16, React 19, TypeScript |
| Database | PostgreSQL (pgx driver) |
| Email Integration | Gmail API (OAuth 2.0 PKCE) |
| Testing | Go Unit Tests, Database Regression Tests, Playwright |
| Environment | Docker Compose / Local PostgreSQL |
The codebase contains more than 16,000 lines of Go backend code and approximately 15,900 lines of TypeScript frontend code. Testing covers backend logic, database operations, and browser-level interactions. Since real banking records contain sensitive personal information, I also built a synthetic test data generator and a Playwright mock API environment, allowing the application to be tested and demonstrated end-to-end without exposing actual banking data.
Where it stands.
Delivered
- Local-First Architecture: Intentionally designed as a local-first, single-user system running on 127.0.0.1 with Docker Compose and PostgreSQL to keep sensitive banking data sovereign.
- Secure Gmail OAuth 2.0 PKCE integration with custom http.RoundTripper read-only transport guards.
- Raw RFC 822 MIME storage in PostgreSQL enabling deterministic historical ledger replaying.
- Exact decimal monetary handling end-to-end without floating-point precision loss.
- Asset registry distinguishing internal account movements from actual expenditures.
- Automatic transaction fee bifurcation and balance reconciliation engine.
- Personal finance dashboard in Next.js 16 and React 19 with Playwright mock testing environments.
What remains
- Local-First Architecture: The application is intentionally designed as a single-user system on 127.0.0.1; it does not provide public cloud hosting or multi-user authentication to protect personal credentials.
- Email-Based Updates: The system depends on bank notification emails rather than direct banking APIs; transaction updates depend on bank email delivery timing.
- Privacy-Friendly Testing: Live banking records are not included in the repository; synthetic transaction fixtures and mocked APIs are used for testing.
