# Introduction

> IndexedDB never leaves the browser it was written in. s3nd is the piece that carries it across.

Canonical: https://doc.s3nd.sh/docs · Markdown: https://doc.s3nd.sh/docs.md

Move a local-first app's data from one device to another, through your own bucket.

```ts
import { createBucket } from '@s3nd/core'

const store = createBucket({ bucket: 'my-bucket', prefix: 'snapshots' })

// On the old device: hand the user a code.
const code = store.codes.create() // "K7QP2M4X"
await store.putSnapshot(code, state, { app: 'notes', version: 3, expiresIn: 3600 })

// On the new device: they type it in.
const snapshot = await store.getSnapshot(store.codes.normalize(typed), { maxVersion: 3 })
snapshot?.data // → the state, ready to write back into IndexedDB
```

## The problem

Local-first is a good trade. Keep everything in IndexedDB and the app is instant, works offline,
and nobody's notes sit on your server by default.

Then the user gets a new phone.

IndexedDB is scoped to one origin _in one browser profile on one device_. There is no sync, no
export, no "sign in and it appears". Clearing site data wipes it. Safari evicts it after seven
days without a visit. From the user's side this reads as "the app lost my data" — which is,
functionally, what happened.

Building the way out is where local-first apps stall: you need a place to put the bytes, a way for
the second device to find them, and enough structure that a restore does not corrupt the app.

## What s3nd does

It is the server-side piece of exactly that. Your app hands its local state to your own API; that
route stores a **snapshot** in a bucket you control, under a short **sync code**; the other device
sends the code back and gets the state.

- **Credentials stay on your server.** The browser only ever talks to your API. No CORS policy on
  the bucket, no signing in the client, no S3 keys shipped to a phone.
- **Snapshots are self-describing.** What lands in the bucket carries the app name, your schema
  version, the device that wrote it and its timestamps — so a restore can refuse data it cannot
  read instead of corrupting the database.
- **Codes are made to be typed.** [Crockford base32](https://www.crockford.com/base32.html), no
  characters people confuse, folded on the way back in: `oil5` and `0IL5` find the same snapshot.
- **Concurrent writes are detectable.** Two devices writing the same key is a race; `ifMatch`
  turns it into an error you can handle rather than data you silently lose.
- **Compression is on by default.** An IndexedDB dump is repetitive JSON, so gzip routinely cuts
  it by 5–10× — which is what keeps it under your runtime's request limit.

## What it is not

It is **not a sync engine.** There is no operational transform, no CRDT, no per-record merge.
A snapshot is the whole state at a point in time, and restoring replaces what is on the receiving
device. That is the right model for "move my data to my new phone" and the wrong one for "two
people edit the same document live". If you need the latter, you want a CRDT library — and
s3nd can still be where you park its state.

It is **not a general S3 client.** No `list()`, no bucket administration, no lifecycle rules. It
hands you its underlying client through `store.client`, so the AWS SDK is always one command away.

It is **not encryption.** Your server can read every snapshot it stores. If that is not acceptable
for your data, encrypt it in the browser and hand s3nd the ciphertext — see
[end-to-end encrypted sync](https://doc.s3nd.sh/docs/use-cases/encrypted-sync).

## Where to go next

- [Quick start](https://doc.s3nd.sh/docs/quick-start) — the two routes, and the client half around them.
- [Snapshots](https://doc.s3nd.sh/docs/snapshots) — the envelope, compression, schema versions, expiry.
- [Move to a new device](https://doc.s3nd.sh/docs/use-cases/new-device) — the flow end to end.
- [Setting up a server](https://doc.s3nd.sh/docs/server) — the middle piece configured properly: auth, expiry, limits.
- [Without a server](https://doc.s3nd.sh/docs/no-server) — machine to machine, the CLI straight to Cloudflare R2.
