# Quick start Source: https://selfstore.dev/docs/quick-start From npm install to a persisted, synced, backed-up browser app in about five minutes, with no server, using the selfstore simple store. selfstore's front door is one call. `selfstore(app)` opens a store that OWNS your data: you read and write collections through it, and saving, syncing and multi-device merge happen on their own. ## Try it first, install after [Open the playground in StackBlitz](https://stackblitz.com/github/selfstoredev/selfstore/tree/main/examples/playground): notes that survive a reload, a real file on your disk as their home, and a genuine encrypted ZIP. Nothing to install, and the page it runs on has no backend either. One caveat worth knowing before you click the file button: the file picker needs the File System Access API, which a cross-origin preview iframe does not get. Open the preview in its own tab first. ## 1. Install ```sh npm install selfstore ``` ESM only, TypeScript types included, browser-first. Three small runtime dependencies (`fflate`, `hash-wasm`, `idb`); the compression and crypto ones load lazily, so they stay out of your critical bundle until first used. ## 2. A working app in five lines ```ts import { selfstore } from 'selfstore'; type Todo = { id: string; text: string }; const store = await selfstore<{ todos: Todo }>('todo-app'); await store.put('todos', { id: crypto.randomUUID(), text: 'ship it' }); store.all('todos'); // read your data back (typed) store.onChange(() => render(store.all('todos'))); // your writes AND other devices ``` `await selfstore(app)` resolves once the store is ready: data loaded from the IndexedDB working copy, any connected destination restored, first sync done. Auto-save (debounced), save on tab hide, and sync on tab focus and network return are already wired. In tests and SSR, where there is no IndexedDB, the same call lands on an in-memory cache, so this exact code runs under vitest. > **The one rule that matters:** every record needs a non-empty **string** > `id`. `put()` throws a `TypeError` right away if it is missing or not a > string, naming the collection and the fix, instead of letting the record > silently never sync. See [the string-id rule](#the-string-id-rule). ## 3. Read and write ```ts store.all('todos'); // readonly array of every record store.get('todos', 't1'); // one record by id, or undefined await store.put('todos', todo); // insert or replace one (auto-saves) await store.putAll('todos', todos); // many in one save await store.remove('todos', 't1'); // delete one (propagates to other devices) await store.clear('todos'); // empty a collection ``` Your objects stay plain JSON: no proxies, no injected fields, no ORM. `all()` returns a readonly snapshot; you change data through `put`/`remove`, and `onChange` fires afterwards, whether the change came from you, another tab, a device sync, or a restore. ## 4. Show the save state The store is headless: it ships stable keys, you ship the copy and colours. ```ts store.subscribe(() => { statusBar.textContent = t(store.status.labelKey); // e.g. 'status.synced' if (store.error) toast(t(store.error.labelKey)); // e.g. 'error.authExpired' }); ``` ## 5. Sync across the user's devices One call, the same on every device. It connects a destination the **user** owns and tells you exactly what happened. ```ts import { gisDriveAuth } from 'selfstore'; const outcome = await store.connectDrive(gisDriveAuth({ clientId })); // or await store.connectFile(); // a disk file (Chromium) // or await store.connectWebdav({ url, username, password }); // Nextcloud & co // or await store.connectS3({ endpoint, region, bucket, key, accessKeyId, secretAccessKey }); // S3, R2, B2, MinIO switch (outcome) { case 'started': break; // destination was empty: this device's data now lives there case 'merged': break; // it held a backup: both sides folded, nothing lost case 'manual': break; // no file access here: offer store.downloadBackup() case 'cancelled': break; // the user closed the picker / consent } ``` If the destination holds an **encrypted** backup and you passed no password, the call throws `PASSWORD_REQUIRED` before touching anything: prompt, then retry with `connectDrive(auth, { password })`. ## 6. Encrypt end to end, and hand over a real file ```ts await store.protect('a passphrase the user chose'); // end-to-end from here on await store.unprotect(); // reversible await store.downloadBackup(); // a portable .zip (encrypted while protected) await store.importBackup(pickedFile); // read one back into the store ``` The download is a genuine ZIP. Without a password it opens in any archive tool; with one it is AES-256-GCM over an Argon2id-derived key, and the [format is publicly specified](https://selfstore.dev/docs/format). ## The string-id rule Every record keys the multi-device merge on a non-empty **string `id`**. The simple store enforces it at `put()`: ```ts await store.put('todos', { id: crypto.randomUUID(), text: 'good' }); // Your records key on another field? Map it per collection: const crm = await selfstore('crm', { sync: { ids: { contacts: 'uuid' } } }); ``` ## Where next - **The mental model:** [concepts](https://selfstore.dev/docs/concepts). - **Build it step by step:** [the tutorial](https://selfstore.dev/docs/tutorial) writes an encrypted notes app from an empty folder to a synced, shareable one. - **Durable homes in depth:** [disk file](https://selfstore.dev/docs/disk), [Google Drive](https://selfstore.dev/docs/google-drive), [WebDAV](https://selfstore.dev/docs/webdav), [S3-compatible](https://selfstore.dev/docs/s3). - **Drop-in UI:** [the connect/share widgets](https://selfstore.dev/docs/widgets) are web components you theme with CSS. - **A sensitive app** (health, legal): [the hardening kit](https://selfstore.dev/docs/sensitive) makes encryption and a strong password non-optional and locks the local cache. - **Own the data model yourself** (Svelte runes, Redux) or write a custom destination: [advanced](https://selfstore.dev/docs/advanced). **Bind status to your UI:** [framework one-liners](https://selfstore.dev/docs/frameworks). Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt