This is the written companion to Video 7 on the channel. If you would rather watch me type the thing live, the video is embedded at the bottom. If you would rather read code, this is the post.
The build: a lead tracker for a service business. Form on top, a list of leads in the middle, status filters across the top, basic search, and a way to mark a lead as "called", "booked", or "dead." Everything stored in localStorage. Total time at the keyboard, with no preparation: 1 hour 27 minutes.
A working internal tool that is good enough to actually use. No deploy. No login. No database. One HTML file.
Why localStorage is the right answer for this build
If you are a one-person shop, the cost of a real backend is rarely worth the time. You do not need to share data with anyone, you do not need to access it from another device, and you do not need it to survive your laptop being run over. You need a way to stop losing leads in your text messages.
localStorage gives you:
- Persistent state across browser refreshes.
- No dependencies. No npm install. No deploy.
- Backup is "right-click → save as" on the HTML file plus a copy of the JSON blob.
The 90-minute version skips authentication, multi-device sync, and any server. The 6-month version, if you keep using it, gets a Supabase row store under the same UI in another 90 minutes. We will get to that.
The whole thing in three files-worth of code
I prefer one HTML file for builds like this — copy/paste-able, no toolchain. Here is the entire data layer:
type LeadStatus = 'new' | 'called' | 'booked' | 'dead'
type Lead = {
id: string
name: string
phone: string
source: string
status: LeadStatus
note?: string
createdAt: number
}
const KEY = 'alphaprism.leads.v1'
function loadLeads(): Lead[] {
try {
return JSON.parse(localStorage.getItem(KEY) ?? '[]') as Lead[]
} catch {
return []
}
}
function saveLeads(leads: Lead[]): void {
localStorage.setItem(KEY, JSON.stringify(leads))
}Three observations on this snippet that are easy to miss:
- The
KEYis versioned (v1). When you change the schema later, you bump tov2and write a one-time migration on load. Saves a ton of pain. JSON.parseis wrapped in a try/catch. If your storage gets corrupted (browser crash, manual edit, etc.), you fall back to an empty list rather than throwing.Leadis typed in a way that maps cleanly to a database row if you ever swap the storage backend.
The UI is a single form + a filterable list. The most interesting piece is the filter, because it is what makes the tool actually usable past 20 leads:
function filterLeads(leads: Lead[], status: LeadStatus | 'all', query: string): Lead[] {
const q = query.trim().toLowerCase()
return leads.filter((lead) => {
if (status !== 'all' && lead.status !== status) return false
if (!q) return true
return (
lead.name.toLowerCase().includes(q) ||
lead.phone.includes(q) ||
lead.source.toLowerCase().includes(q)
)
})
}That is it. No fuzzy match, no debouncing yet, just a substring filter over three fields. If you find yourself wanting more, you have probably outgrown localStorage and the right next step is a real database.
The bits I'd do differently if you plan to keep it
The 90-minute version is good enough to use tomorrow. But three sharp edges show up around the two-week mark:
- No multi-device sync. If you do anything from your phone, the laptop's
localStoragedoesn't know. The right fix is a Supabase row store + an anonymous device id + an "this is my dataset" passphrase. About 90 more minutes. - No backup. Browsers occasionally clear
localStorageon their own (disk full, profile reset, "Clear browsing data"). You will not get a warning. A nightly "download backup" button that pushes the current JSON to your Downloads folder costs about 20 minutes and saves the dataset from extinction. - No undo. It is a leads list, not a spreadsheet — but the moment a customer says "you marked me dead by mistake," you want an undo. A small
actions[]ring buffer in storage solves it. About 15 minutes.
The full source
The build ships as index.html — open in a browser, you have a lead tracker. No build step, no deps. Companion video walks through every line.
Want a real one built for your business?
The 90-minute build is fun. But if you are running an actual operation, you probably want the version with multi-device sync, role-based access, undo, a real backup story, and a backend that doesn't depend on a single browser profile staying alive.
That is the same kind of system we build for clients in two to three weeks, hosted on the client's own accounts.