There's a specific kind of developer frustration that hits when you're building something small — a personal tool, a tracker, a quick utility — and you catch yourself configuring a database, writing auth middleware, and deploying to a server before you've written a single line of actual product logic.
At some point I asked myself: does this actually need a backend?
For a surprising number of things, the answer is no.
localStorage Is Not a Toy
localStorage gets written off as a place to stash JWT tokens and dark mode preferences. But it's a synchronous key-value store that persists across sessions, survives page refreshes, holds up to ~5MB per origin, and works completely offline. For personal tools — the kind you use yourself, on your own machine — that's often everything you need.
The mental model shift is this: instead of thinking "where do I store this data?", ask "does anyone else need to read this data?" If the answer is no, localStorage is a legitimate first-class storage layer, not a placeholder until you set up Postgres.
The Architecture: One File, No Build Step
When I built a job application tracker, I made a deliberate choice to keep it as a single HTML file. No bundler. No framework. No npm. Just a .html file you can open in a browser.
The structure is roughly:
-
<style>block for everything visual -
<script>block with all the logic - A thin wrapper around localStorage for reads/writes
- A render function that rebuilds the UI from state on every change
That last point is the key pattern. Instead of surgically updating the DOM, I keep a state object in memory, persist it to localStorage on every mutation, and re-render the relevant section. It's not React — it's just a renderJobs() function that wipes and rebuilds a list. For an app with a few hundred records, this is imperceptibly fast.
Here's the core read/write pattern, stripped to its essence:
const STORAGE_KEY = 'job_tracker_data';
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : { jobs: [] };
} catch {
return { jobs: [] };
}
}
function saveState(state) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
// Usage
let state = loadState();
function addJob(job) {
state.jobs.push({ ...job, id: Date.now(), createdAt: new Date().toISOString() });
saveState(state);
renderJobs();
}
That's the whole persistence layer. No ORM, no migrations, no connection strings. The try/catch around JSON.parse handles corrupted data gracefully. Using Date.now() as an ID is fine for personal tools — you're not worried about distributed ID collision.
What You Give Up (Honestly)
This architecture has real limitations and pretending otherwise would be dishonest.
No sync across devices. Your data lives in one browser on one machine. Open the same file on your phone and you start fresh. For a job tracker this is usually fine — you're applying from a laptop. For anything collaborative or multi-device, you need something else.
No sharing or team features. If you want to show someone your pipeline or share a list, you're copying JSON or screenshotting. There's no shareable link, no invite system.
No server-side queries. If you reach 10,000 records and need to do complex filtering, you're doing it in-memory in JavaScript. This scales surprisingly well, but there's a ceiling.
No automatic backup. If someone clears browser storage, the data is gone. You can mitigate this with an export-to-JSON button (which I added), but it's opt-in. This is genuinely worth being upfront about.
What You Actually Gain
The list of things you don't have to deal with is worth enumerating:
- No hosting costs
- No auth system
- No database to provision, back up, or migrate
- No GDPR concerns about storing user data on a server
- Works on an aeroplane
- Ships as a file attachment
- Zero dependencies to go stale
- Nothing to deploy
There's also something psychologically clean about it. The app does exactly one thing and owns exactly the storage it needs. Users who are privacy-conscious appreciate that nothing ever leaves their browser. For a job tracker especially — where you're storing salary expectations, notes about companies, interview feedback — that matters to some people.
Surprising Use Cases for This Pattern
Once you internalize "single file + localStorage", you start seeing places it fits:
- Personal finance trackers — private by nature, one user, one machine
- Reading lists or book logs — no need for a Goodreads account
- Habit trackers — daily use, offline, no subscription
- Simple CRMs for freelancers who don't need Salesforce
- Client meeting notes when you don't want them in a cloud
The pattern is especially well-suited to anything where you are the only user and privacy is a feature, not an afterthought.
Single File vs. Modules
The obvious objection is maintainability — doesn't a 2000-line HTML file become a nightmare?
Somewhat, yes. For a production app with a team, absolutely use modules, a bundler, and proper separation of concerns. But for a personal tool you build once and maintain lightly, the single-file constraint is liberating. Everything is in one place. You can Ctrl+F your way to anything. You can email it to yourself, open it anywhere, never worry about whether dependencies are installed.
It's the same reason people still use spreadsheets for things that "should" be a database. The friction is low enough that they actually use it.
Building vs. Using
If you're interested in the architecture, I hope this gives you enough to experiment with. The pattern is genuinely underused for personal tooling.
If you're in a UK job search right now and would rather just use something than build it — I put together UK Job Tracker Pro, which is exactly this: a single HTML file, works offline, no account, no signup, free. Tracks applications, statuses, contacts, notes. Everything stays in your browser.
The honest summary: localStorage-first apps aren't for everything. But for personal tools where you're the only user, they're often the right call — and developers underestimate them because we're trained to think infrastructure is the default. Sometimes the right database is already in the browser.
Top comments (0)