Working offline

Saving data in an HTML app: localStorage, IndexedDB and files

A bundled app has no server to save to. Where its data actually lives, what survives an update or a reboot, the limits of each option, and how to let the user back their data up.

8 min read Updated September 2026

An HTML app inside an APK saves data with the same web storage APIs a browser offers — localStorage for small key-value state and IndexedDB for anything structured or large — and both persist across app restarts, phone reboots and app updates. They are cleared only when the user clears the app's data or uninstalls it. There is no server, so there is nothing to sync unless you add one; the trade-off is that the data lives on that one phone, which is why an export button is worth adding to anything the user would mind losing.

The options

localStorageIndexedDBDownloaded fileCookies
StoresStrings, by keyObjects, blobs, indexesAnything, as a file the user keepsSmall strings
Size~5 MBHundreds of MB (quota-managed)Unlimited4 KB
APISynchronous, two linesAsynchronous, verbose (use a wrapper)Blob + download linkAwkward
Survives app updateYesYesYes (it is outside the app)Yes
Survives uninstallNoNoYesNo
Use forSettings, progress, small listsNotes, records, images, anything that growsBackups, exportsNothing, in a bundled app

localStorage: the default

// save
localStorage.setItem('settings', JSON.stringify({ theme: 'dark', sound: true }));
// load, with a fallback for the first run
const settings = JSON.parse(localStorage.getItem('settings') || '{"theme":"light","sound":true}');

Synchronous, simple, and enough for most small apps. Two habits: always JSON.stringify on the way in and parse on the way out (it stores strings only), and always supply a fallback, because the first launch has nothing saved. Keep individual values small — writing a 2 MB string on every keystroke will jank the UI. The starter file includes a two-line save/load helper that does exactly this.

IndexedDB: when it grows

For a notes app, a collection, a journal, anything with hundreds of records or with images, use IndexedDB. Its raw API is famously verbose; a small wrapper such as idb-keyval (a 600-byte library you can download and inline) turns it into await set('note-42', note) and await get('note-42'). Storage quota on Android is generous — typically a percentage of free disk — and the browser asks nothing of the user until it is nearly full.

What "persistent" means in practice

Backup: give the user a file

Because the data lives on one phone, the kindest feature you can add is export and import. Export is a download:

function exportData() {
  const blob = new Blob([JSON.stringify(localStorage)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'my-app-backup.json';
  a.click();
}

Inside the APK, that download goes to the phone's Downloads folder, where the user can share it to Drive, email or another phone. Import is a file input that reads the JSON back and writes each key. Ten lines each, and the app's data can outlive the phone.

What does not work

fetch('data.json') to read a file from the bundle fails inside an APK — local-file requests are blocked by the WebView's origin rules. Inline the data as a JavaScript object instead (const DATA = {…} in a script tag), or embed it in a hidden element. Writing files directly to the phone's storage from JavaScript is not possible either; the download pattern above is the way. Cookies work but have no advantage over localStorage here. Service workers and the Cache API do not register from a bundled page — and are unnecessary, since the bundle is already offline.

Questions people ask

Does localStorage data survive an app update?

Yes, provided the update has the same package name and signing key. Android keeps the app's data directory; only uninstalling or clearing data removes it.

How much can I store?

localStorage is limited to about 5 MB. IndexedDB can hold hundreds of megabytes on a modern phone, subject to the device's free space.

Can two of my apps share data?

No. Each app's web storage is private to it. To move data between apps, export a file from one and import it in the other.

Why does fetch('data.json') fail inside the app when it works on my server?

Because the page is loaded from local storage, and the WebView blocks requests from that origin to local files. Put the data in a script tag as a JavaScript object instead.

Is the data encrypted?

It is inside the app's private directory, which other apps cannot read on an unrooted phone, but it is not encrypted at rest. Do not store passwords or secrets there; see the security guide.

Read next

Your HTML, installed on a phone today

Upload the file or ZIP, pick a name and an icon, and download a signed Android APK in minutes. Free to start — no Android Studio, no code changes, no card.

Convert HTML to APK — free