# 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