Guide · Architecture

Tools with no backend, and the honest trade

A tool that never sends data anywhere cannot leak it, cannot lose it in someone else's breach, and cannot be repriced when the hosting bill arrives. It also cannot sync to your phone. Both halves are worth saying out loud.


The default is heavier than the problem

Most small utilities get built with an account system, a database, a session store, and a hosting bill — for a calculator. The reflex is so automatic that nobody asks whether the server is doing anything the browser could not.

For a large class of tools, it is not. A position sizer, a trade journal, a weight tracker, a habit checklist, a unit converter, a text formatter: all of these are pure functions plus a little state. The browser has a JavaScript engine and persistent storage. That is the entire requirement.

Which tools actually need a server

The honest test is not "can this be a website" — almost anything can. It is whether the feature the tool provides is possible without a second party holding state. A decision table, run once per tool, saves the account system nobody needed.

Tool typeNeeds a serverWhy
Calculator, converter, position sizerNoPure function of the inputs. Nothing to persist across a session at all.
Trade journal, habit tracker, weight logNoState is single-user and can live entirely in browser storage.
Unit or file conversion (images, text, small media)NoJavaScript or WebAssembly can do the transform in the browser.
Team task board, shared documentYesMultiple people must see the same state, which requires a relay.
Chat or messagingYesDelivery to someone else's device requires a server in the middle.
Marketplace or listings appYesInventory must stay consistent across every buyer viewing it.
Anything calling a paid API with a secret keyYesA key embedded in browser JS is public the moment the page loads.
Anything that must survive a lost or wiped deviceYes, or strict export disciplineA server-side copy is the only unconditional guarantee.
Cross-device sync as a hard requirementYes, or an opt-in sync layerCovered later — this does not have to mean starting over.

What you gain

No breach surface

The strongest security property available is not holding the data. No user table means no credential dump, no password reset flow to phish, no session tokens to steal, no compliance obligation around personal data you never received. You cannot be forced to hand over records that were never on your server, and an attacker who fully compromises the host gets a folder of static files.

Data stays where the user can see it

Browser storage is inspectable. A curious user can open DevTools and read exactly what the app kept. That is a much stronger claim than a privacy policy, because it is verifiable rather than promised — the network tab shows there are no outbound requests carrying their data.

It costs nothing to run

Static files on GitHub Pages, Netlify, or Cloudflare Pages cost nothing at realistic personal-project traffic. There is no database to back up, no dependency to patch at 2am, no bill that grows with users. A client-side tool that gets popular gets cheaper per user, not more expensive.

It keeps working

No server means no outage, no rate limit, no deprecation. Offline through a service worker, it works on a plane — the mechanics of that are in the offline PWA guide. A tool built this way in 2020 still runs today with no maintenance, which is not a claim most SaaS side projects can make.

Choosing a storage mechanism

"Browser storage" is not one thing. Picking the wrong one shows up later as either a size ceiling or a jank problem, both avoidable up front.

MechanismPersists after tab closePractical sizeSync to serverUse it for
CookiesYesAbout 4 KB per cookieSent automatically on every requestServer communication, not app state — the wrong tool for local-only data
sessionStorageNo, cleared on tab closeAbout 5 MBNoneTransient in-page state, never saved data
localStorageYesAbout 5 MB per originNoneSmall JSON — journals, logs, settings, a few thousand records
IndexedDBYesLarge, a share of free diskNoneStructured or binary data, real queries, anything past a couple MB
Cache APIYesLargeNoneNetwork responses and assets behind a service worker, not app data
Origin Private File SystemYesLargeNoneFast binary file access; newer, support still catching up

What you give up

This is the part most local-first pitches skip, so here it is plainly.

You loseWhat that means in practice
Cross-device syncData logged on the laptop is not on the phone. Two devices means two separate datasets.
BackupClearing browser data deletes everything. On iOS, storage can be evicted after long non-use.
Server-side computeNo heavy jobs, no scheduled tasks, no calling a paid API without exposing the key.
CollaborationNo sharing, no multi-user, no permissions — there is nothing to share through.
Analytics on usageYou genuinely do not know how the tool is used, which makes it harder to improve.
RecoveryNo support ticket can restore what a user deleted, because you never had a copy.

The honest framing is not that these do not matter. It is that for single-user tools they usually matter less than the ongoing cost of an account system — for the builder and for the user, who has one more password and one more breach notification in their future.

The honest threat model

"Local-only" is not a synonym for "secure." State plainly what the architecture protects against and what it does not — overclaiming here is exactly the kind of promise that erodes trust the moment someone technical tests it.

ThreatProtected?Why
Server-side data breachYesThere is no server-side copy of user data to breach
Database credential leakYesThere is no database
Password reuse or credential stuffingYesThere is no password, because there is no account
XSS on your own domainNoA script injected into the page reads localStorage and IndexedDB with the same access your own code has
Malicious or compromised browser extensionNoExtensions with broad permissions can read page-accessible storage like any other script
Shared or public deviceNoAnyone with access to the browser profile can open DevTools and read everything stored
Physical device theft, unencrypted diskNoBrowser storage is not encrypted by the browser itself; disk encryption is a separate, OS-level setting
Compromised third-party script or CDN dependencyNoA compromised script runs with the same privileges as the rest of the page

The defence against every "No" row is ordinary web security hygiene, not anything specific to local-first: a strict Content-Security-Policy, subresource integrity on third-party scripts, sanitising anything rendered as HTML, and treating browser extensions as ambient risk outside your control. None of that becomes optional just because there is no server to also secure — the client is now the entire attack surface rather than a thin layer in front of one, which is exactly the case a tool like DIRA is built to scan for.

Mitigations that keep the model intact

  1. Ship export and import. A JSON or CSV download turns 'no backup' into 'your backup, your file'. This is the single highest-value feature in a local-only tool.
  2. Warn before destructive actions and never auto-clear storage on version change.
  3. Make the storage key stable across deploys, and migrate schemas forward rather than resetting them.
  4. Say where the data lives, in the app, not only in the privacy policy. One line under the form does more than a page nobody opens.
  5. If sync is genuinely needed later, add it as an opt-in layer over the same local store rather than rewriting around a server.

Schema versioning, worked

"Migrate schemas forward" above is the mitigation people skip, because it sounds like more work than it is. Here is what it looks like end to end, for a tracker that adds a unit field in version 2 that version 1 records never had.

const STORE_KEY = 'cut-tracker'; const CURRENT_VERSION = 2; const MIGRATIONS = { 1: (data) => data, // baseline, nothing to do 2: (data) => ({ ...data, entries: data.entries.map(e => ({ ...e, unit: e.unit || 'lb' })), }), }; function load() { const raw = localStorage.getItem(STORE_KEY); if (!raw) return { version: CURRENT_VERSION, entries: [] }; let data = JSON.parse(raw); let v = data.version || 1; while (v < CURRENT_VERSION) { v += 1; data = MIGRATIONS[v](data); } if (v !== (data.version || 1)) { data.version = v; localStorage.setItem(STORE_KEY, JSON.stringify(data)); } return data; }

Two rules make this safe. Migrations run forward, one version at a time, so a jump from version 1 to version 4 replays three small, individually testable steps instead of one large rewrite. And the migration writes back to storage immediately, so the very next load is already current — which means the migration code for an old version can eventually be deleted once you are confident nobody is still on it.

Adding sync later, without a rewrite

The last mitigation above deserves the full explanation, because "add sync later" sounds harder than it is if the local store was treated as the source of truth from day one.

  1. Local storage stays the source of truth even after sync exists. Sync is a mirror, not a replacement — the app must keep working with the network off.
  2. Sync is an explicit opt-in, never a silent background upload. A user who never enables it is running the exact same app as before sync existed.
  3. Sync diffs, not the whole dataset. Timestamp every record on write, and push only what changed since the last successful sync.
  4. Pick a conflict rule before you need one. Last-write-wins by timestamp is simple and fine for single-user data edited from one device at a time; genuinely concurrent editing needs a CRDT or operational-transform library, which is real added complexity, not a checkbox.
  5. The server side can be minimal. One authenticated endpoint that accepts and returns a JSON blob per user is enough for years of a single-user tool. It does not need to become a product.
async function sync() { if (!syncEnabled()) return; const last = localStorage.getItem('lastSyncAt') || 0; const dirty = getEntries().filter(e => e.updatedAt > last); if (dirty.length) { await fetch('/api/sync', { method: 'POST', body: JSON.stringify({ entries: dirty }) }); } const remote = await fetch('/api/sync?since=' + last).then(r => r.json()); mergeByTimestamp(remote.entries); // last-write-wins on updatedAt localStorage.setItem('lastSyncAt', Date.now()); }

Nothing about this touches the local storage schema, the UI, or any code path a non-syncing user exercises. That is the actual payoff of designing local-first from day one: sync becomes an addition, not a rewrite.

What client-side hosting actually costs

The zero-cost claim made earlier is worth pricing out explicitly, against the alternative, as an illustrative comparison rather than a quote from any specific host.

HostFree tierWhat it costs at real personal-project trafficBest for
GitHub PagesUnlimited for public reposEffectively $0Personal tools, portfolios, static guides
Cloudflare PagesUnlimited bandwidth on the free planEffectively $0Same as GitHub Pages, with a faster edge network
Netlify100 GB bandwidth per month free$0 until traffic passes the free allowance, then billed per GBStatic tools that also want form handling
VercelGenerous free tier tuned for frameworks$0 until usage forces an upgradeIf you are already building with a framework it favours
Any backend (VM plus managed database)Rarely a real free tierRoughly $5 to $50+ per month minimum, before any trafficOnly once a specific feature has proven it needs a server

The comparison is not close. Static hosting is not merely cheaper than a backend, it is frequently exactly $0 at the traffic a personal tool actually receives, while a backend carries a monthly floor whether or not anyone opens the tool that day.

Writing the privacy policy honestly

A local-only tool makes for a short, unusually credible privacy policy — but only if it is specific. Vague reassurance reads exactly like every policy that turned out to be false.

  • Name the storage mechanism: 'stored in your browser's localStorage on this device'.
  • State plainly that there is no account, no server-side database, and no transmission of entered data.
  • Disclose what is collected, if anything — static hosting still produces server logs with IP addresses, and any embedded analytics or fonts are third-party requests. Do not claim zero collection while loading a tracker.
  • Explain the eviction risk: clearing site data deletes it, and browsers may evict storage. Point at the export button in the same sentence.
  • Skip the legal-sounding padding. A policy a user can read in 60 seconds is more protective than one they cannot.

The test I use: could a technically literate user verify every claim in the policy from the network tab in under a minute? If yes, it is honest. If a claim requires trusting me, it should be softened or removed. Mine is here →

When you actually do need a backend

Not every tool fits. You need a server when the data is genuinely shared between people, when compute exceeds what a phone browser can do, when an API key must stay secret, when the data must survive a lost device, or when a regulation requires audit trails you can produce. Those are real requirements — the point is to check whether you have one, not to assume you do. Cut, on the stack described here, is the small end of the local-only case: a weight tracker with nothing behind it but a browser.

Tools referenced in this guide

  • Apps hub — three tools built this way — no accounts, no server, no analytics on your entries.
  • Cut — weight tracker with local storage and an export path.
  • Privacy policy — a short, verifiable version of the claims described here.
  • My stack — what static hosting and tooling this runs on.

FAQ

Quick answers

What is a client-side-only tool?

A tool that runs entirely in the browser, with all state kept in browser storage such as localStorage or IndexedDB, and no server component beyond serving static files. There is no account, no database, and no transmission of what the user enters.

Is a tool without accounts more secure?

For the user's data, generally yes, because the strongest protection is not holding the data at all. There is no user table to dump, no session tokens to steal, and no password reset flow to phish. It does not make the code itself more secure, and it offers no protection if the device is compromised.

What do you lose with no backend?

Cross-device sync, server-side backup and recovery, collaboration between users, server-side compute, secret API keys, and any real usage analytics. For single-user tools these usually matter less than the cost of running an account system, but they are real losses and should be disclosed.

What does it cost to run a client-side tool?

Effectively nothing. Static files on GitHub Pages, Netlify, or Cloudflare Pages are free at personal-project traffic levels, and there is no database to back up or patch. Cost per user falls as usage grows rather than rising.

How do users back up data in a browser-only app?

Through an export function that downloads their data as JSON or CSV, which they store themselves. This is the single most important feature in a local-only tool, because clearing site data deletes everything and browsers can evict storage after extended non-use.

What should a local-only privacy policy say?

Name the exact storage mechanism, state that there is no account and no server-side database, disclose anything that is still collected such as hosting server logs or third-party fonts and analytics, explain that clearing site data deletes everything, and point at the export button. Every claim should be verifiable from the browser's network tab.

Does a client-side-only tool protect against XSS?

No. Cross-site scripting on your own domain reads localStorage and IndexedDB with the same access your own code has, so a local-only architecture protects against a server-side breach but not against a script injected into the page. That defence is ordinary web hygiene — a strict Content-Security-Policy, subresource integrity, and sanitising anything rendered as HTML — not something local-first replaces.

How do you add sync to a local-only tool without rewriting it?

Keep local storage as the source of truth, add sync as an explicit opt-in rather than a silent upload, push only records changed since the last sync using a timestamp on each record, and pick a conflict rule upfront — last-write-wins is enough for single-user data edited one device at a time. The server side can be a single endpoint that stores a JSON blob per user; it does not need to become a product.