# Concepts Source: https://selfstore.dev/docs/concepts The selfstore mental model - the simple store, snapshots, durable homes, backups, sync and the headless status - in one page. selfstore has one mental model, and once it clicks the whole API reads naturally.
Your data flows through the store to an IndexedDB working copy, leaves the device only as an encrypted ZIP to a home you own, and other devices converge at that same home. Your data JSON records + binary files put / all / remove The store selfstore('app') : owns + auto-saves working copy Working copy IndexedDB, sealed at rest, offline-first encrypted ZIP (AES-256-GCM) A home you own disk / Drive / WebDAV / S3 same home = meeting point Other devices deterministic on-device merge
There is no server in this picture. The cloud (if you use one) holds only an opaque encrypted ZIP; every merge runs on your devices.
## The simple store `const store = await selfstore('app')` is the front door. It OWNS the data: you `put` and `all` through it, and it handles saving, syncing and merging. Convention over configuration, every default overridable: - an IndexedDB cache named after the app (in-memory where IndexedDB does not exist, so tests and SSR just work), - schema version 1, - debounced auto-save on every mutation, - the browser sync moments (tab focus, network return, an interval, tab hide) wired automatically (`autoSync: false` to take over). Everything deeper lives on `store.advanced` - the full `createLocalStore` store, the same instance. Start simple; reach down without rewiring. That deeper surface is the [advanced guide](https://selfstore.dev/docs/advanced). ## Snapshot The unit the backup and merge layers exchange: ```ts type Snapshot = { collections: Record; // named arrays of plain JSON records files: { id: string; name: string; mime: string; bytes: Uint8Array }[]; }; ``` You rarely build one by hand with the simple store, but it is what a backup file contains and what `gather()`/`apply()` move on the advanced store. Two constraints: every record carries a **string id** (or you map one via the [sync config](https://selfstore.dev/docs/sync)), and collection names starting with `__` are reserved for the library's own bookkeeping. ## Working copy The IndexedDB cache that makes offline the normal case. Writes land there (debounced); `selfstore(app)` restores from it on open. It is **encrypted at rest**: your records and file blobs are AES-256-GCM envelopes under a non-extractable per-device key, unsealed only in memory as your app reads them. That defeats casual inspection, partial exfiltration and disk forensics; it does not stop code running in your own origin. For data where even a full copy of the browser profile must stay sealed, [cacheLock](https://selfstore.dev/docs/security#cachelock) keeps the key in memory only. ## Durable home Where the data survives a cleared browser: a disk file, the user's Google Drive, any WebDAV server, an S3-compatible bucket. Attached with one call and one user gesture (`store.connectFile()` / `connectDrive(auth)` / `connectWebdav(config)` / `connectS3(config)`), rebuilt silently on the next open. The home receives an encrypted ZIP; the cloud only ever holds opaque bytes. Connecting resolves to an honest [`ConnectOutcome`](https://selfstore.dev/docs/quick-start): `merged`, `started`, `manual` or `cancelled`. A home is any object implementing `BackupTarget`; the built-ins are conveniences, not privileges ([write your own](https://selfstore.dev/docs/advanced)). ## Backup A portable, self-describing ZIP of one snapshot. `store.downloadBackup()` and `store.exportBackup()` produce one; `store.importBackup(file)` reads one in. Unencrypted it opens in any archive tool; encrypted (while `protect()` is on) it is AES-256-GCM over an Argon2id-derived key with the parameters stored per file. The [format](https://selfstore.dev/docs/format) is specified independently of the library. ## Sync The same home, connected from a second device, becomes a meeting point. A Hybrid Logical Clock plus per-collection strategies converge replicas deterministically, on-device; the storage stays dumb. Conflicts are journaled with both values instead of silently dropped. Details: [sync](https://selfstore.dev/docs/sync) and [peers](https://selfstore.dev/docs/peers) for sharing between people. ## Headless status One descriptor drives your persistence UI: `store.status` (`{ state, severity, actionable, action?, labelKey }`) and `store.error` (`{ code, labelKey, message } | null`). No colours, no copy: you map `labelKey` through your i18n and `severity` through your design tokens. The `action` field is load-bearing. A transient failure (an offline blip, a cold-starting server) is **not** attention-worthy: the store retries silently. Only a genuine loss of access sets `action`, and only to one of two gestures: `unlock` (`store.unlock(password)` for a locked encrypted home) or `reconnect` (`store.reconnect()` after real auth loss). ## Schema versions `schema` versions your **data shape**, not your app release. Bump it when the shape changes and pass `migrate(fromVersion, snap)`; older backups then upgrade on read. Data written by a **newer** schema than the running app refuses loudly with `SCHEMA_TOO_NEW` instead of corrupting silently. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt