Skip to content
s3nd
Menu

Use case

Move an app to a new device

A local-first app with no accounts. A code on the old phone, typed into the new one, and the whole database follows.

app/api/sync/route.ts
import { store, SCHEMA_VERSION } from '@/lib/store'

export async function POST(request: Request) {
  const state = await request.json()
  const code = store.codes.create()

  await store.putSnapshot(code, state, {
    app: 'notes',
    version: SCHEMA_VERSION,
    device: request.headers.get('user-agent') ?? undefined,
    expiresIn: 60 * 60,
    ifAbsent: true,
  })

  return Response.json({ code })
}

The situation

What is actually going on.

The problem

The user has a new phone. The app on the old one holds everything, in IndexedDB, and there is no account to sign into because the app never needed one. IndexedDB does not leave the browser it was written in.

What s3nd does about it

The old device dumps its object stores, posts them to your API, and gets an eight-character code back. The new device types the code, sees what it is about to restore, and replaces its empty database in one transaction. The snapshot expires within the hour and the code is burned on success.

Worth watching

The things that are easy to get wrong.

Confirm before replacing

Return createdAt and device from the lookup, and show them. The most common mistake is restoring onto the device that already had the data.

Version the snapshot

Pass maxVersion on read. A snapshot from a newer build of the app fails with a clear error instead of landing in a version that will misread it.

Burn the code

A DELETE after a successful import is the tidy path; the expiry is the backstop. Both are one line.

Packagess3nd@s3nd/react

Put a file. Hand over the code.

Point it at the bucket you already pay for. Nothing to deploy, nothing to sign up for, nothing in the middle.

npm install -g @s3nd/clis3nd init --provider r2 --bucket drops3nd put ./anything.zip