# Continuous backup

> One snapshot per account, rewritten as the local database changes, with conditional writes so two devices cannot silently clobber each other. How to build it with s3nd, what to watch, and the full guide.

Canonical: https://www.s3nd.sh/use-cases/continuous-backup · Markdown: https://www.s3nd.sh/use-cases/continuous-backup.md · Français: https://www.s3nd.sh/fr/use-cases/continuous-backup

One snapshot per account, rewritten as the local database changes, with conditional writes so two devices cannot silently clobber each other.

```ts
const current = await store.getSnapshot(`user-${userId}`)

try {
  await store.putSnapshot(`user-${userId}`, merged, { ifMatch: current?.etag })
} catch (error) {
  if (isS3ndError(error) && error.code === 'PRECONDITION_FAILED') {
    // Another device won. Read again, merge again.
  }

  throw error
}
```

## What is actually going on.
### The problem
Once the app has accounts, a transfer code is the wrong shape. There is a session, so the server already knows who is asking. What the user wants is for their data to survive losing the laptop, without doing anything.

### What s3nd does about it
Key the snapshot by user id instead of by a code, and rewrite it whenever the local database changes. Pass the ETag you last read as ifMatch: a second device that wrote in the meantime makes the write fail with PRECONDITION_FAILED instead of silently discarding its work, and your app reads again and merges.

## The things that are easy to get wrong.
- **A different lifecycle rule** — Backups must not expire. Keep them under their own prefix so the rule that cleans up transfers cannot reach them.
- **Debounce the writes** — A snapshot per keystroke is a bill. Write on a timer, on visibility change, and before unload.
- **Merging is yours** — s3nd tells you that you lost the race. Deciding what a merge means for your data is application code, which is why it is not hidden.

Packages: s3nd. [Continuous backup, in full](https://doc.s3nd.sh/docs/use-cases/continuous-backup)
