Skip to content
Build your own tools

← Build guide

Keeping data

Anything should survive closing the tab: settings, entered data, imported files.

Section 5 Plain text (.md)

There is no server, so there is no central database — and that is usually the point. The only question is what survives closing the tab.

NeedUseLimit
Settings, last selection, a draftlocalStoragestrings only, a few MB
Imported files, parsed records, long listsIndexedDBclumsy API
Real queries across tablesSQLite compiled to WebAssemblyextra library, real effort

Pick the lowest rung that works. Most tools never leave localStorage.

localStorage

var KEY = 'toolname.settings';

function saveSettings(obj) {
  try { localStorage.setItem(KEY, JSON.stringify(obj)); } catch (e) {}
}

function loadSettings() {
  try { return JSON.parse(localStorage.getItem(KEY)) || {}; }
  catch (e) { return {}; }
}

Two habits that prevent later grief: prefix every key with the tool name, and wrap every read — the stored value may come from an older version, or the browser may refuse storage entirely (private window, blocked site data). A tool that throws on startup because of a stale setting is a tool nobody opens twice.

IndexedDB

For parsed data sets, imported files, anything beyond a few hundred kilobytes. The raw API is unpleasant enough that a small CDN wrapper is the one justified exception to “write it yourself”.

It works everywhere, including from a double-clicked file. It needs no OPFS and no cross-origin isolation (COOP/COEP). If you find yourself reaching for those headers, the approach is wrong for this context.

SQLite in the browser

Only for genuine analysis — grouping, joins, multi-year comparison. The pattern that holds up: open the database in memory, run the schema and the queries, then serialise the whole database to a byte array and store that in IndexedDB; restore it on the next start. No special headers required, so it still works on a static host.

This is a multi-session project, not an evening. Only propose it when the analysis genuinely needs SQL.

Asking the browser not to throw it away

Browsers evict site data under disk pressure, and a tool that parked a parsed dataset in IndexedDB is a fat target. You can ask for it to be spared:

async function keepStorage() {
  if (!navigator.storage || !navigator.storage.persist) return false;
  try { return await navigator.storage.persist(); } catch (e) { return false; }
}

async function storageLeft() {
  if (!navigator.storage || !navigator.storage.estimate) return null;
  try { return await navigator.storage.estimate(); }   // { usage, quota }
  catch (e) { return null; }
}

Treat a false as the normal case, not an error. It is a request, not a setting: the browser decides, it may refuse without explanation, and from a double-clicked file it may not be granted at all. Feature-check both, wrap both, and never let either one block startup.

So it does not change what you must tell the user below — it only improves the odds. An export button is still the actual backup.

What the tool must tell the user

Device-bound storage has consequences that belong in the UI, not in a footnote:

So: an export button is mandatory as soon as a tool keeps anything. Offer import of the same format, and the user has a backup and a way to move between devices. See Saving results.