RafiFinance
A complete personal finance tracker built as a single HTML file. No server, no database, no framework. Transactions, budgets, saving goals, health score, 6-month analysis — works offline as a PWA.
RafiFinance v4 is a finance tracker I built because every other option was either too complicated, required an account, or did something weird with my data. So I made my own — in a single HTML file.
So I built my own with a deliberate architectural constraint: one HTML file. No npm. No build step. No server. No framework. Open it in a browser and it works. Your data stays on your device in localStorage under the key rfv4, exportable as a single JSON file at any time.
Deployed on Cloudflare Pages at finance.rafiarsya.com — free hosting, global CDN, automatic HTTPS, zero config. The PWA service worker is generated as a Blob URL at runtime, so even the offline capability requires no external files.
The health score is a weighted composite of three independent signals about your financial habits:
score = (savingsRate × 0.4)
+ (budgetAdherence × 0.35)
+ (goalProgress × 0.25)
-- savingsRate (0–100):
savings = totalIncome - totalExpenses
rate = savings / totalIncome × 100
normalized = clamp(rate / 20 × 100, 0, 100)
-- saving 20% of income = 100 points
-- budgetAdherence (0–100):
for each category with a budget:
adherence_i = clamp(1 - overspend_i/budget_i, 0, 1)
budgetAdherence = mean(adherence_i) × 100
-- goalProgress (0–100):
for each saving goal:
progress_i = saved_i / target_i
goalProgress = mean(progress_i) × 100
score = clamp(score, 0, 100)The weights reflect real financial priorities: savings rate has the largest impact (40%) because building a buffer is the foundation of financial health. Budget adherence (35%) prevents spending creep. Goal progress (25%) rewards intentional saving behavior — it carries less weight than the other two because not having an active goal yet shouldn't tank an otherwise healthy score.
# Monthly income RM 3,000, expenses RM 2,400
savings = 3000 - 2400 = 600
rate = 600/3000 × 100 = 20%
savingsRate (normalized) = clamp(20/20 × 100) = 100
# Budgets: Food RM400 (spent 380), Transport RM150 (spent 165)
adherence_food = clamp(1 - 0/400, 0, 1) = 1.00
adherence_transport = clamp(1 - 15/150, 0, 1) = 0.90
budgetAdherence = mean(1.00, 0.90) × 100 = 95
# One goal: 1200/5000 saved
goalProgress = (1200/5000) × 100 = 24
score = 100×0.4 + 95×0.35 + 24×0.25
= 40 + 33.25 + 6
= 79.25 → 79 / 100monthsRemaining = (deadline.year - today.year) × 12
+ (deadline.month - today.month)
monthlyRequired = (targetAmount - savedSoFar) / monthsRemainingThis calculation updates live as transactions come in. If you overshoot in a month, the next month's target drops automatically — the system always shows the shortest path to the goal rather than a fixed plan set once and forgotten. Falling behind one month means the remaining months simply absorb a slightly higher target, recalculated the moment the app reopens.
# Goal: Save RM 5,000 for a laptop
savedSoFar = RM 1,200
deadline = 8 months from today
monthlyRequired = (5000 - 1200) / 8
= 3800 / 8
= RM 475 / month
# After month 1, saved an extra RM 600 (RM 1,075 total):
monthsRemaining = 7
monthlyRequired = (5000 - 1075) / 7
= RM 560.71 / month ← recalculated automaticallyThe entire app — HTML structure, ~800 lines of CSS, ~1800 lines of vanilla JavaScript — lives in one HTML file. All state is managed with a single global object persisted to localStorage:
{
"transactions": [
{ "id": "t_1710000000000",
"type": "expense", // "income" | "expense"
"amount": 45.50,
"category": "Food",
"date": "2026-06-01",
"note": "Lunch at cafe"
}
],
"budgets": {
"Food": 400,
"Transport": 150,
"Entertainment": 100
},
"goals": [
{ "id": "g_1",
"title": "New Laptop",
"target": 5000,
"deadline": "2026-12-01",
"saved": 1200
}
],
"reminders": [ ... ],
"settings": { "currency": "MYR", "theme": "dark" }
}The entire state serializes to a single JSON string. Export = JSON.stringify(state) → file download. Import = JSON.parse(fileContent) → merge into state. One-line backup/restore, with no server round-trip and no account system required — your financial data never leaves the device unless you explicitly export it.
# Export button click:
const blob = new Blob(
[JSON.stringify(state, null, 2)],
{ type: 'application/json' }
);
// → downloads "rafifinance-backup-2026-06-19.json"
# Import on a new device:
const state = JSON.parse(fileContent);
localStorage.setItem('rfv4', JSON.stringify(state));
location.reload();
// → all transactions, budgets, goals restored instantlyThe PWA manifest is embedded inline as a data: URL. The service worker is generated from a Blob URL at runtime — no external sw.js file. Everything self-contained:
<link rel="manifest" href="data:application/json,{...}"> — the manifest JSON is encoded directly into the HTML, no separate file needed.Blob, then registered via URL.createObjectURL(blob) — no separate sw.js file on the server.