# Tutorial - an encrypted notes app Source: https://selfstore.dev/docs/tutorial Build a real notes app step by step - from an empty file to offline notes, a home the user controls, end-to-end encryption and cross-device sync - one call at a time, with no server. This tutorial builds a working notes app in the order the features actually arrive: notes that persist, then a home the user owns, then end-to-end encryption, then sync - each one a single call added to the last step. Every snippet is real; paste them in order and you have the app. There is no server at any point. ## Step 0 - a page Any bundler, or none. An HTML file with an import map is enough to run selfstore straight from npm's CDN: ```html
``` With a bundler, `npm install selfstore` and `import { selfstore } from 'selfstore'`. ## Step 1 - a store that owns the notes One call opens the store. It loads any saved data, wires auto-save, and resolves when it is ready: ```ts const store = await selfstore('notes-app'); const add = (text) => store.put('notes', { id: crypto.randomUUID(), text, at: Date.now() }); document.getElementById('new').addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.value.trim()) { add(e.target.value.trim()); e.target.value = ''; } }); ``` `put` auto-saves (debounced) to an IndexedDB working copy. That is already a persistent app: type a note, reload the page, it is still there. No server, no fetch. ## Step 2 - render, and re-render on every change `onChange` fires after any change to the data - your writes, and later, edits arriving from another tab or device. Render from `all()`, which returns a readonly snapshot: ```ts function render() { const notes = [...store.all('notes')].sort((a, b) => b.at - a.at); document.getElementById('list').innerHTML = notes .map((n) => `
  • ${escapeHtml(n.text)}
  • `) .join(''); } store.onChange(render); render(); ``` Notice the shape of the data: plain objects with a string `id`. That `id` is the one rule - it is what the multi-device merge keys on, so `put` throws right away if it is missing. Everything else is yours. ## Step 3 - a home the user controls So far the notes live only in this browser. To keep them across the user's devices, connect a durable **home** the user owns. The quickest to demo is a file on disk (Chromium); the same shape works for Drive, WebDAV and S3: ```ts document.getElementById('connect').addEventListener('click', async () => { const outcome = await store.connectFile(); // opens a save dialog if (outcome === 'manual') { // Non-Chromium: no direct file access. Offer store.downloadBackup() instead. } }); ``` From now on, every change also writes to that file. Point a second device at the same file (or the same Drive folder) and their notes converge - see step 5. Prefer not to hand-roll the picker? Drop in the [connect widget](https://selfstore.dev/docs/widgets) and it renders the whole journey, styled as your own. ## Step 4 - encrypt it end to end A backup should be unreadable to whatever holds it. One call turns on AES-256-GCM over an Argon2id-derived key; from here the home only ever sees ciphertext: ```ts await store.protect(passphraseTheUserChose); // await store.unprotect(); // reversible ``` Download the result and inspect it: `store.downloadBackup()` hands you a real `.zip`. Without a password it opens in any archive tool; with one, it is [the specified encrypted format](https://selfstore.dev/docs/format), ciphertext through and through. ## Step 5 - sync across devices (and tabs) There is no step 5 to write. Once two devices point at the same home, they converge on their own: on tab focus, on network return, on a slow interval, and when you call `store.sync()`. Open the app in a second tab right now, edit a note, and watch the first tab update - that is the same merge that runs between devices, in miniature. Concurrent edits resolve by a logical clock (last writer wins), and deletions propagate. The [sync guide](https://selfstore.dev/docs/sync) has the model and how to surface a conflict. ## Where to take it - **Show the state** - a saving/synced/offline indicator: [the status descriptor](https://selfstore.dev/docs/quick-start#4-show-the-save-state). - **Share it** - hand another person a link to read or co-edit: [peers and groups](https://selfstore.dev/docs/peers). - **Harden it** - make encryption and a strong password non-optional and lock the local cache: [sensitive apps](https://selfstore.dev/docs/sensitive). - **Frameworks** - the same store binds to React, Svelte and others in a few lines: [framework bindings](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