Skip to content

Repository files navigation

Expense Tracker

A multi-account personal expense tracker and financial dashboard that runs entirely on Cloudflare's edge — Workers for compute, D1 (SQLite) for storage, and Workers Static Assets for the dashboard. No VPS, no container, no build step. Upload a bank/card statement CSV and get spend analytics, balance tracking, and net-worth — served from the edge.

License Platform Build

Privacy: this repository is code only. It ships with synthetic sample statements (samples/) — no real financial data is included, and your own data (uploaded to your own deployment) lives only in your private D1 database.


Contents


What it does

You bank across several accounts — credit cards, a savings account — and each one hands you a CSV or statement export every month. This app ingests those statements into one permanent, deduplicated ledger and turns them into a dashboard that answers two questions at once:

  1. Where does my money go? — categorized spend, top merchants, monthly/yearly trends, filterable by account and time period.
  2. Where do I stand? — current vs previous credit-card balances, credit utilisation, payment-due alerts, EMIs/loans, savings balance over time, cash flow, and overall net worth (assets − liabilities).

It is designed as a multi-account system from the ground up: adding a new account (another card, another bank) is an upload, not a code change, and adding a new account type is a small, additive extension rather than a rewrite.

Features

  • Multi-account — credit cards (liabilities) and savings accounts (assets), each a first-class account with its own identity, tracked side by side.
  • Account-wise analytics — view all accounts together, a single account, or a multi-select subset. The selection is shared across the Monthly and Yearly views.
  • Spend analytics — category breakdowns, top merchants, daily/monthly/yearly trends, KPIs.
  • Balance & net-worth tracking — per-card current/previous balance, utilisation, nearest due date, savings balance-over-time, and a net-worth strip combining assets and liabilities.
  • Cash flow — money-in vs money-out and net cash flow for accounts that report a running balance (savings).
  • Smart categorization — regex rules stored in the database and editable without a redeploy. Money that merely moves (transfers, credit-card bill payments, ATM withdrawals, investments) is auto-classified and excluded from "spend" by default, with a toggle to include it.
  • Deduplication — a SHA-256 hash per transaction means re-uploading the same or an overlapping statement never creates duplicates.
  • Audit trail & rollback — every upload is logged; a bad import can be rolled back, removing exactly its rows and nothing else.
  • Zero build step — the dashboard is plain HTML/CSS/JS + Chart.js from a CDN, served as static assets straight from the Worker.

Architecture

                      ┌────────────────────────────────────────────────┐
  Browser  ─────────▶ │  Cloudflare Worker  (src/index.ts, Hono)        │
  (dashboard,         │                                                  │
   static assets)     │  POST /api/upload                                │
                      │     └▶ parsers.ts   detect format, parse rows,   │
                      │     │               derive account identity      │
                      │     └▶ pipeline.ts  normalize dates, clean        │
                      │     │               merchants, SHA-256 dedup,    │
                      │     │               regex categorization         │
                      │     └▶ D1 (INSERT OR IGNORE + statement/EMI      │
                      │                  snapshot + account registry)    │
                      │                                                  │
                      │  GET /api/summary/*, /api/accounts/*, …          │
                      │     └▶ D1 queries ─▶ JSON ─▶ Chart.js            │
                      └────────────────────────────────────────────────┘
                                          │
                                          ▼
                          D1 (SQLite): imports · transactions ·
                          accounts · category_rules · statements ·
                          loans · column_mappings
  • src/parsers.ts — format detection and parsing. Ships with three parsers (HDFC pipe-delimited card statements, HDFC savings CSV, and a generic header-based CSV) plus a statement-metadata parser for balances/limits/EMIs. Adding a bank is one more tryParseX() function; the pipeline below is untouched.
  • src/pipeline.ts — date normalization, merchant cleanup, a deterministic SHA-256 dedup hash per row, and regex-rule categorization loaded from the category_rules table.
  • src/index.ts — the Hono app: the upload endpoint (the whole ingestion pipeline in one batched, dedup-safe write) plus the read endpoints that back the dashboard.
  • public/ — the dashboard: plain HTML/CSS/JS + Chart.js from a CDN. No framework, no bundler — served as static assets from the Worker.
  • migrations/ — the D1 schema, applied in order.

Tech stack

Layer Choice
Runtime Cloudflare Workers (V8 isolate, nodejs_compat)
Framework Hono 4
Storage Cloudflare D1 (SQLite at the edge)
Dashboard Vanilla HTML/CSS/JS + Chart.js 4
Language TypeScript 5 (strict), no emit — Wrangler bundles the Worker
Tooling Wrangler

Project structure

expense-tracker/
├── src/
│   ├── index.ts        # Hono app: upload + all read endpoints, query helpers
│   ├── parsers.ts      # format detection, statement parsers, statement-meta/EMI parser
│   └── pipeline.ts     # date/merchant normalization, dedup hash, categorization
├── public/
│   ├── index.html      # dashboard markup (Overview / Balances / Monthly / Yearly / Transactions / Imports)
│   ├── app.js          # dashboard logic + Chart.js rendering
│   └── style.css       # styles
├── migrations/
│   ├── 0001_init.sql             # imports, transactions, category_rules, column_mappings + seed rules
│   ├── 0002_statements.sql       # statements (balances) + loans (EMIs)
│   ├── 0003_accounts.sql         # accounts registry + account_key/type/balance_after on transactions
│   └── 0004_savings_categories.sql  # transfer/CC-payment/ATM/investment category rules
├── samples/            # synthetic statements for testing the pipeline (see samples/README.md)
├── wrangler.jsonc      # Worker + D1 + static-assets config
├── tsconfig.json
├── package.json
├── AGENTS.md           # conventions & constraints for contributors and AI agents
└── CONTRIBUTING.md

Data model

All tables live in D1 (SQLite). See migrations/ for the authoritative schema.

Table Purpose
imports One row per upload — filename, format, counts, status. Powers the audit trail/rollback.
transactions The permanent ledger. UNIQUE(txn_hash) enforces dedup at the DB level.
accounts Registry of every account (account_key, account_type, label, institution, last4).
category_rules Regex → category, priority-ordered, editable at runtime.
statements Per-card, per-cycle balance snapshot (authoritative for "what do I owe").
loans Per-statement EMI/loan snapshot, for paydown tracking.
column_mappings Reserved for future custom CSV profiles.

Key concepts:

  • account_key (e.g. Card_6081, Savings_7464) is the universal discriminator that joins transactions to the account registry. account_type is credit_card (a liability — balance is what you owe) or savings (an asset — balance is what you have).
  • Spend vs cash flow. "Spend" = debits only, and money-movement categories (Transfers, Credit Card Payment, ATM/Cash, Investments) are excluded by default so a ₹2L transfer doesn't dwarf real spending and a card-bill payment isn't double-counted. Pass includeTransfers=1 to fold them back in. "Cash flow" counts both directions across all categories.
  • Balances are sourced, not summed. Credit-card balances come from the statement's authoritative Account Summary (statements table), not from summing transactions. Savings balances come from the per-row running balance_after.
  • Dedup hash = SHA-256(account | timestamp | description | amount | direction). Same-day distinct transactions survive; re-uploads collapse to no-ops.

Getting started (local)

Prerequisites: Node.js ≥ 18, npm, and a (free) Cloudflare account. Wrangler is installed as a dev dependency — no global install needed.

git clone https://github.com/Lalit-Patil-07/expense-tracker.git
cd expense-tracker
npm install

npm run db:create        # creates a D1 database and prints its database_id
# paste that id into wrangler.jsonc → d1_databases[0].database_id

npm run db:migrate:local # applies all migrations to the LOCAL D1
npm run dev              # http://localhost:8787

Open http://localhost:8787, then upload a file from samples/ to see the dashboard populate. npm run typecheck type-checks the Worker without emitting.

Deploy to Cloudflare

npx wrangler login       # one-time browser auth
npm run db:create        # once, if you haven't already — paste the id into wrangler.jsonc
npm run db:migrate:remote # create the tables on the real (remote) D1
npm run deploy           # ship the Worker + dashboard

Your dashboard goes live at https://<your-worker>.<your-subdomain>.workers.dev.

wrangler.jsonc ships with a "<your-d1-database-id>" placeholder. Replace it with the id from npm run db:create before deploying. (A D1 database_id is not a secret — access still requires your Cloudflare account login — but each fork uses its own database.)

Usage

The dashboard has six views: Overview (all-time), Balances (net worth, card balances, due alerts, EMIs), Monthly, Yearly, Transactions (searchable/filterable/paginated), and Imports (audit trail + rollback).

Monthly workflow:

  1. Download your bank/card statement (CSV or the HDFC statement export).
  2. Dashboard → + Upload statement → pick the file.
  3. The pipeline parses, cleans, deduplicates, categorizes, and (for card statements) records the balance snapshot and any EMIs. The dashboard refreshes. New data is added to history — prior months are never altered.

For savings/bank exports that carry no account number inside the file, the account identity is derived from the filename (see below), so keep the bank's original filename.

Supported statement formats

Format is auto-detected on upload (see parseStatement in src/parsers.ts):

Format Looks like Account identity
hdfc_pipe_statement HDFC credit-card export, `~ ~`-delimited, with an Account Summary
hdfc_savings_csv HDFC savings CSV: Date, Narration, …, Debit, Credit, Closing Balance derived from the filename (e.g. Acct_Statement_XXXXXXXX7464_….csv → Savings_7464)
generic_csv Any CSV with Date + Description + Amount (or Debit/Credit) a Card/Account column, if present

API reference

All endpoints are under /api. Read endpoints accept optional filters; unknown/absent filters mean "all". accounts is a comma-separated list of account_keys; includeTransfers=1 opts money-movement categories back into spend.

Method & path Description
POST /api/upload Ingest a statement (multipart file, or raw body + ?filename=).
GET /api/imports Upload audit trail.
DELETE /api/imports/:id Roll back one import (deletes only that import's transactions).
GET /api/transactions Filterable/searchable/paginated ledger (from,to,category,account,accounts,merchant,q,limit,offset).
GET /api/summary/overview All-time KPIs, category/account/month breakdowns, top merchants.
GET /api/summary/monthly?year=&month= Daily spend, categories, totals, by-account, cash flow for a month.
GET /api/summary/yearly?year= By-month, categories, totals, by-account, cash flow for a year.
GET /api/categories Category rules (regex, priority, enabled).
POST /api/categories/rules Add a rule {category, pattern, priority?} — no redeploy needed.
GET /api/accounts Account registry + per-account txn counts and date spans.
GET /api/accounts/overview Net position: assets (savings), liabilities (card dues), net worth.
GET /api/balances/overview Per-card current/previous balances, utilisation, due dates, history.
GET /api/statements Raw statement history (all cards).
GET /api/loans Latest snapshot of each active EMI/loan.

Extending it

  • New bank/statement format — add a tryParseYourBank(text, opts): ParseResult | null in src/parsers.ts and register it in the parseStatement attempts array (order matters: more-specific parsers first). The clean/dedupe/categorize pipeline is unchanged.
  • New category — POST /api/categories/rules with {category, pattern, priority}, or add a migration seeding category_rules. Rules are regex, matched case-insensitively against the uppercased description, lowest priority first. Takes effect on the next upload.
  • New account type — the model keys everything on account_key + account_type, so a new type (wallet, loan account, brokerage) is an additive migration + a parser that tags rows with the new type, not a rewrite.

See AGENTS.md for the detailed conventions and invariants behind each of these.

Development workflow

npm run dev          # local Worker + dashboard at http://localhost:8787
npm run typecheck    # tsc --noEmit — run before every commit
  • Make changes, then verify against samples/ in the local dashboard (never against real/prod data).
  • Local D1 (.wrangler/, gitignored) is disposable — roll back test uploads from the Imports view.
  • Branch, commit with a clear message, and open a PR. See CONTRIBUTING.md.

Data-safety guarantees

  • No duplicates — every transaction's hash is unique in the DB; re-uploading a statement (or an overlapping month) is a no-op for rows already present.
  • No accidental loss — uploads only ever INSERT OR IGNORE. Nothing is overwritten or deleted by a normal upload. The only delete path is an explicit per-import rollback.
  • Audit trail — every upload is logged in imports (filename, timestamp, rows parsed/inserted, duplicates, errors) even if later rolled back.

Contributing

Contributions are welcome — see CONTRIBUTING.md for setup, conventions, and the PR process, and AGENTS.md for architecture, invariants, and the hard Cloudflare Workers runtime constraint.

License

Licensed under the GNU General Public License v3.0 or later — see LICENSE. Copyright (C) 2026 Lalit Patil.

About

Multi-account personal expense tracker and financial dashboard on Cloudflare Workers + D1 — spend analytics, balance & net-worth tracking, no build step.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages