Storage Landscape

Pick storage by ownership, query shape, platform, and recovery path. Do not start with the trendiest sync library.

StoreBest forAvoid whenRecovery story
SQLiterelational local app state, app documents, desktop/CLI datamany concurrent writers or untrusted DB imports without validationonline backup, VACUUM INTO, copy after checkpoint, export tables
RoomAndroid relational app state with Kotlin APIstiny preferences or cache-only dataschema export, migration tests, app-managed export
IndexedDBbrowser structured data, blobs, offline queuestiny settings, cross-origin sharing, guaranteed non-eviction without persistenceJSON/blob export, Dexie export/import, server sync
OPFSbrowser file-like data and worker-heavy writesuser-visible files or broad browser compatibility assumptionsapp-managed export to user-selected file
JSON/JSONLappend-only logs, configs, transcripts, simple portable filesrelational queries, multi-record transactions, high write concurrencycopy file, validate checksum/schema, import line by line
Markdownhuman-owned notes and knowledgestrict schema or high-volume queriesuser can read and edit directly; indexes are derived
Secure storekeys, tokens, small secretslarge data setsplatform-specific account/device recovery path
Remote DBshared server truth, collaboration, cross-device syncoffline-only or privacy-first v1server backups plus local cache recovery

SQLite

SQLite is the safest default for app-local relational data because it gives transactions, constraints, indexes, and a single durable file. Use WAL for app-local concurrency, then use the SQLite backup API or VACUUM INTO for live backups.

Use SQLite as an app document when users need one portable file with relational structure. Treat the schema as a file format contract.

Room

Room is the Android default for non-trivial structured local data. It adds compile-time query validation, DAOs, entities, database classes, and migration helpers over SQLite.

Room does not remove storage responsibility. You still need schema export, migration tests, export/import, backup decisions, and encryption/key management.

IndexedDB And OPFS

Use IndexedDB for browser-local structured data. It is asynchronous, transactional, same-origin scoped, supports indexes, and can store blobs. Use a wrapper such as Dexie when you want cleaner transactions, schema versions, and export/import helpers.

Use OPFS when you need file-like browser-local storage, especially from a worker. Do not use OPFS as the only user recovery story because it is origin-private.

Desktop Paths

Desktop apps must choose deliberate data locations:

StackPath rule
Tauriuse app config/data paths and plugin permissions; do not scatter DB files in working directories
Electronuse app.getPath('userData') with app-specific subdirectories; keep cache/session data separate from user data
Native appsfollow OS app data conventions and document backup locations

File-Backed State

Files beat databases when the user benefits from direct inspection and edits. Good examples include Markdown notes, JSON configs, JSONL transcripts, and attachment folders.

Use a database index next to files when you need fast search. Keep the index rebuildable.