Skip to main content

Command Palette

Search for a command to run...

I Built an Offline-First Expense Tracker in One 13.5KB HTML File — No Framework, No Backend, No Subscription

Updated
8 min read
I Built an Offline-First Expense Tracker in One 13.5KB HTML File — No Framework, No Backend, No Subscription
J
Hi, I’m Joy — a designer, software engineer, and problem-solver with 6+ years of real-world experience. I specialize in creating intuitive designs, building software, and fixing complex technical issues. I love diving deep into problems and finding smart, efficient solutions. Alongside my work, I’m also a college student, constantly learning and growing in the tech space. I’m passionate about sharing what I learn, exploring new tools, and helping others navigate challenges in design and development.

Last year I had my best month ever as a freelancer — and then discovered, two hours deep into tax-season spreadsheet archaeology, that my real profit margin was 11% and I'd set aside exactly $0 for taxes.

My first instinct was the normal one: subscribe to an accounting app. Then I looked at what I actually needed — log income, log expenses, show profit, multiply by a tax percentage — and had the thought that derails every developer eventually:

This is a CRUD app. Why would I rent a CRUD app for $15/month, forever?

So I built my own. The constraints I chose turned out to be the most interesting engineering decisions I made all year, and the result — SoloLedger — is a complete money tracker for freelancers that ships as a single HTML file, 13.5 kilobytes, runs fully offline on any device, and stores nothing on any server because there is no server.

This post is about the constraints, the patterns that made them workable, and what shipping a single-file app taught me about local-first software in 2026.

The constraints (or: requirements as a rage response)

Every tool I evaluated failed at least one of these, so they became the spec:

  1. No subscription. A money tracker that bills you monthly is an expense tracker with a built-in expense. One-time purchase or nothing.

  2. No backend. My income data, client categories, and spending patterns should not live in someone else's Postgres. Also: no backend means no hosting bill, no auth system, no breach surface, no GDPR processor agreements. The features you don't build can't leak.

  3. No build step, no framework. If the app is simple enough to be one file, a 40MB node_modules directory is a confession of failure.

  4. Offline-first, actually. Not "works offline after the service worker caches 4MB of chunks." A file on your device that opens in a browser. Airplane mode is not an edge case for freelancers who work from anywhere.

  5. One file. The distribution format is the file. Email it to yourself, drop it on any phone, tablet, or laptop, tap it, it runs. The file is the app, the installer, and the database client.

That last constraint is the one people push back on, so let's start there.

Why a single HTML file is a legitimate architecture

We've normalized the idea that a web app is a deployment: a domain, a CDN, a bundle graph, a backend, an uptime dashboard. But for a whole class of apps — personal tools where the data belongs to one human — that stack mostly serves the vendor, not the user.

A single HTML file with inline CSS and JS gives you:

  • Radical portability. Any browser since roughly 2017, on Android, iOS, or desktop, executes it identically. Add to home screen and the OS treats it like a native app.

  • Permanence. There's no server to shut down. If I get hit by a bus, the app keeps working on every device it's ever been copied to. Compare that with every cloud tool whose shutdown email starts with "we're sunsetting…"

  • Auditable privacy. "We don't upload your data" is a promise. View Source on one file is a proof. Anyone can confirm there isn't a single network call in it.

  • A sane sales model. A file is a product. You can sell it on Gumroad like an ebook. No license servers, no seat management.

The cost is discipline: 13.5KB doesn't happen by accident. It happens because every feature has to justify its bytes — which, it turns out, is a fantastic product-design forcing function.

The stack: vanilla JS, localStorage, and nothing else

The data model is the whole backend:

// One entry. That's it. That's the schema.
{ id, type: 'income' | 'expense', amount, category, note, date }

Entries live in localStorage as a single JSON document, loaded at startup into memory and written back on every mutation. People underestimate localStorage because it's old and synchronous, but for a personal finance tracker the math is laughing at us: a heavy user logging 20 entries a day for ten years is ~70k entries — a few megabytes of JSON, well within budget, and parsing it takes milliseconds on a 2019 phone.

State management without a framework collapses into one honest function:

function commit() {
  localStorage.setItem('sololedger', JSON.stringify(state));
  render();
}

Every "state library" you've ever used is this function wearing increasingly expensive clothes. With one screen-ish of UI and a few hundred entries visible at a time, re-rendering from state is instant. No virtual DOM, no diffing, no hooks — render() rebuilds what changed, and the profiler shrugs.

The genuinely fun problems

Twelve currencies for free. Freelancing is global, so SoloLedger handles $, €, £, ₹, ₦, R, ₱, RM, ¥, ₫, KSh, and GH₵. The trick is that the browser already ships a localization engine:

new Intl.NumberFormat(undefined, {
  style: 'currency', currency: userCurrency
}).format(amount);

Intl.NumberFormat costs zero bytes of my budget and formats better than any library I'd have vendored in 2018.

CSV export with no server. Accountants want CSV. The Blob API turns "export" into four lines:

const blob = new Blob([toCsv(state.entries)], { type: 'text/csv' });
const a = Object.assign(document.createElement('a'), {
  href: URL.createObjectURL(blob), download: 'sololedger-export.csv'
});
a.click();

Backup and restore is the same pattern with JSON — the user owns a complete, portable copy of their data at all times. Local-first isn't just storage location; it's an exit door that's always open.

Charts without a charting library. The 6-month profit chart is the feature that tells you whether your hustle is growing or leaking, and the temptation is to npm install 280KB of charting. But a bar chart is rectangles. SVG generated from a template literal renders six bars, crisp on every DPI, for a few hundred bytes. The dashboard — income, expenses, real profit at a glance — is the same discipline applied everywhere: the tax shield (the number that started this whole project) is literally profit * userTaxRate, displayed prominently, recomputed on every commit. The 25–30% that freelancers are told to set aside finally computes itself from net, not from vibes.

The 30-second UX bar. The reason spreadsheets and Notion templates failed me wasn't capability — it was friction. So the logging flow is three taps: type, amount, category, with freelancer-native categories (client work, retainers, product sales, ads, platform fees) instead of "Groceries." If daily logging takes longer than unlocking the phone, the system dies by February. UX latency is retention.

Privacy as architecture, not policy

SoloLedger has no account system, no analytics, no telemetry, and no network calls. Not because a privacy policy says so — because the architecture makes the alternative impossible. There is nothing to breach, nothing to subpoena, nothing to sell, and no quarterly incentive for me to "expand data collection."

I think this matters beyond one small app. We're a few years into the local-first software conversation, and most of it focuses on CRDTs and sync engines — the hard distributed-systems end. But there's a humbler tier that's almost embarrassingly underexplored: apps that simply don't need sync. A personal ledger is one human, one dataset, append-mostly. The simplest local-first app is a local app.

What shipping it taught me

  1. Constraints are the product. Every limitation I chose (one file, no backend, size budget) surfaced in the marketing as a benefit (portable, private, fast). When constraints and positioning are the same sentence, the product explains itself.

  2. Boring tech ships. Vanilla JS, localStorage, Blob, Intl, SVG. Every API in this app is a decade old and will work in a decade. Meanwhile half my 2021 side projects no longer build.

  3. Devs underestimate non-dev problems. "Track freelance profit and taxes" sounds beneath us as an engineering problem. But the search data tells another story — how much should I set aside for taxes self-employed is one of the most-asked money questions on the internet, and every answer is a blog post saying "25–30%, probably." A tool that computes it from your actual net, on your own device, answers it better than another article ever could.

  4. A file can be a business. SoloLedger sells on Gumroad for $19, one-time — no license server, free updates to v2, 30-day money-back, demo data baked in so people can try it with fake numbers first. For launch week it's 50% off with this link: joytxis.gumroad.com/l/sololedger/LAUNCH50. If you freelance or run a side hustle next to your dev job, that's less than a coffee for never wondering what your real profit is again.

The bottom line

The most radical architecture decision available to a web developer in 2026 might be subtraction: no backend, no build, no framework, no subscription. One file that opens in a browser, owns nothing of yours, and outlives its creator.

If you've built (or want to build) something single-file or local-first, tell me in the comments — I want to read every weird 14KB app this community has shipped. And if you just want your freelance money to finally make sense: SoloLedger, 50% off for launch.