# s3nd · Send anything with a code, through your own bucket.

> Send a file, a folder or an app's data from one machine to another with an eight-character code, through an S3 bucket you own. A CLI, a TypeScript library and React hooks. No account, no relay, nothing to deploy. Works with AWS S3, Cloudflare R2, MinIO, Scaleway and Wasabi.

Canonical: https://www.s3nd.sh/ · Markdown: https://www.s3nd.sh/index.md · Français: https://www.s3nd.sh/fr

## The whole product is three commands.
The bytes go from one machine to your bucket and from your bucket to the other. Nothing streams through anyone else’s server, and nobody signs up for anything.

- **put** — Drop it in your bucket. A file, a folder as an archive, whatever comes down stdin. One PutObject into a bucket you own, under a fresh code, with an expiry stamped on the object. The code is printed and nothing else, so it composes. [The CLI reference](https://doc.s3nd.sh/docs/cli)
- **code** — Read it over the phone. Eight characters, forty bits, no I, L, O or U. Write it on a sticky note, type it in the wrong case with a dash in the middle: it still resolves. The server picks it and claims it with a conditional write. [Sync codes](https://doc.s3nd.sh/docs/sync-codes)
- **get** — Pick it up anywhere. Any machine with the code and access to the bucket, or a token for your server, gets the file back. Until the transfer expires, or you burn it. The other machine did not have to be on when you sent. [Without a server](https://doc.s3nd.sh/docs/no-server)

## A terminal, your own app, or anything that speaks HTTP.
One primitive under all three. The server-side package holds the keys; everything that runs in a browser depends on fetch and nothing else.

### From a terminal (@s3nd/cli)
Straight to the bucket with the credentials on that machine, nothing deployed. Or through your server with a token and `--remote`. A doctor command proves the setup works.

```sh
$ s3nd put ./build.tar.gz
build.tar.gz · 41 MB · expires in 1 day
K7QP2M4X

$ tar cz ./photos | s3nd put - --name photos.tar.gz
$ CODE=$(s3nd put ./report.pdf)     # code on stdout, the rest on stderr

# any other machine
$ s3nd get k7qp-2m4x
Wrote ./build.tar.gz · 41 MB
```

### Inside your app (@s3nd/core, @s3nd/react)
One route file serves the four-route protocol on your domain, with your auth. React hooks send a file, read a code back, and repair what the user typed.

```ts
// app/api/transfers/[[...route]]/route.ts
import { createBucket, createTransferHandler } from '@s3nd/core'

export const { GET, POST, DELETE } = createTransferHandler({
  bucket: createBucket({ bucket: 'drop' }),
  expiresIn: 24 * 3600,
  authorize: (request) => request.headers.get('authorization') === `Bearer ${process.env.TOKEN}`,
})

// In the browser, with @s3nd/react:
const { sendFile, transfer } = useSendTransfer()
await sendFile(file) // → transfer.code
```

### Anything with HTTP (@s3nd/protocol)
The protocol is four routes and one error format, written down. curl works, a Go client works, and a server in Rails works with every s3nd client.

```sh
$ curl -X POST https://drop.example.com/api/transfers \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/pdf" \
    -H "X-S3nd-Filename: report.pdf" \
    --data-binary @report.pdf
{ "code": "K7QP2M4X", "kind": "file", "size": 290816, "expiresAt": "…" }

$ curl -LOJ -H "Authorization: Bearer $TOKEN" \
    https://drop.example.com/api/transfers/K7QP2M4X/raw
```

## Nobody in the middle.
Every other tool in this space either runs a relay, hosts your files, or asks for an account. s3nd is a thin layer over object storage you already pay for.

- **No relay**: Machine to bucket, bucket to machine. Your provider’s durability, your provider’s bill, and on R2 no egress fee at all.
- **No account**: A code is the whole handshake. On your own server, a bearer token per person is the most identity s3nd ever asks for.
- **Nothing to deploy**: The CLI talks to the bucket directly. A server enters the picture only when a browser has to, and it is one route file.
- **Expires on its own**: Every transfer carries an expiry, checked on every read. A lifecycle rule deletes the object, and s3nd doctor checks you have one.

- [Cloudflare R2: No egress fees, which is exactly what moving a 2 GB file between two laptops wants.](https://www.s3nd.sh/providers/cloudflare-r2)
- [AWS S3: The original. Credentials come from the provider chain, so a Lambda or an ECS task needs no keys at all.](https://www.s3nd.sh/providers/aws-s3)
- [MinIO: One Docker command, and the real code path runs on your laptop and in CI.](https://www.s3nd.sh/providers/minio)
- [Scaleway Object Storage: European regions, S3-compatible, and the region actually means something.](https://www.s3nd.sh/providers/scaleway)
- [Wasabi: Flat-rate storage with no egress charge, on a regional endpoint.](https://www.s3nd.sh/providers/wasabi)

## A code you can read over the phone.
The whole experience of a transfer is someone reading a code off one screen and typing it into another. Everything about the code is shaped by that.

The default is eight characters of Crockford base32: no `I`, `L`, `O` or `U`, so a code survives paper, a phone keyboard and a phone call. Generation and normalization live on the same object, so the two sides can never disagree about the alphabet.

Four digits for a phone-first app, twelve alphanumerics for a long-lived drop: both halves are configurable, and `entropyBits` tells you what the code is worth guessing against so the rate limit can do the rest.

A code is a bearer token. Give it a short expiry, rate-limit the lookup route, and for sensitive payloads encrypt before anything reaches the bucket.

- [Sync codes](https://doc.s3nd.sh/docs/sync-codes)
- [Configuring codes](https://doc.s3nd.sh/docs/code-configuration)

## Not only files. An app’s whole state.
A transfer can be structured data as well as bytes. That is how a local-first app with no accounts carries its database to the user’s new phone: the browser exports IndexedDB, the server snapshots it, the other phone types the code.

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

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

// On the old phone: 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 phone: they type it in.
const snapshot = await store.getSnapshot(store.codes.normalize(typed), { maxVersion: 3 })
snapshot?.data // → ready to write back into IndexedDB
```

- **A self-describing envelope** — Your app name, your schema version, the device, an expiry, then the data, gzipped. A restore refuses a snapshot from a newer build instead of misreading it, and shows when and where it was made before replacing anything.
- **Two devices, one backup, no silent loss** — Pass the ETag you last read as `ifMatch` and a device that writes after someone else did gets an error instead of overwriting their work.

## What people move with it.
### Files
- [A file between two machines: A laptop, a desktop, a code. No server, no account, through the bucket you already have.](https://www.s3nd.sh/use-cases/file-between-machines)
- [A drop box for your team: One route on your own domain, a token per person, and a code instead of a chat upload.](https://www.s3nd.sh/use-cases/team-drop-box)
- [Backups from CI: A nightly archive to your bucket from a workflow, with secrets from the runner and no file to check in.](https://www.s3nd.sh/use-cases/ci-backups)

### App state
- [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.](https://www.s3nd.sh/use-cases/new-device)
- [Continuous backup: One snapshot per account, rewritten as the local database changes, with conditional writes so two devices cannot silently clobber each other.](https://www.s3nd.sh/use-cases/continuous-backup)
- [End-to-end encrypted: Encrypt in the browser with a passphrase. Your server and your bucket store bytes they cannot read.](https://www.s3nd.sh/use-cases/encrypted-sync)
- [Attachments beside the data: The blobs an app holds, carried across as objects, with the records that point at them in the snapshot.](https://www.s3nd.sh/use-cases/attachments)

## Why not croc, WeTransfer or rclone?
Sometimes they are the right answer. The comparison pages say when, tool by tool, in the other tool’s own terms.

- [vs Magic Wormhole: A Magic Wormhole alternative that goes through your own bucket](https://www.s3nd.sh/alternatives/magic-wormhole)
- [vs croc: A croc alternative with nothing running between the two machines](https://www.s3nd.sh/alternatives/croc)
- [vs WeTransfer: A WeTransfer alternative that keeps the file in your own bucket](https://www.s3nd.sh/alternatives/wetransfer)
- [vs Firefox Send: A Firefox Send alternative that needs nothing deployed](https://www.s3nd.sh/alternatives/firefox-send)
- [vs PairDrop: A PairDrop alternative for when the other device is not in the room](https://www.s3nd.sh/alternatives/pairdrop)
- [vs transfer.sh: A transfer.sh alternative with no service to run](https://www.s3nd.sh/alternatives/transfer-sh)
- [vs Dexie Cloud: A Dexie Cloud alternative for apps that have no accounts](https://www.s3nd.sh/alternatives/dexie-cloud)
- [vs PowerSync: A PowerSync alternative when there is no backend database](https://www.s3nd.sh/alternatives/powersync)
- [vs ElectricSQL: An ElectricSQL alternative for apps without Postgres](https://www.s3nd.sh/alternatives/electricsql)
- [vs Replicache: A Replicache alternative for one-shot moves rather than multiplayer](https://www.s3nd.sh/alternatives/replicache)
- [vs PouchDB: A PouchDB alternative that does not need a CouchDB](https://www.s3nd.sh/alternatives/pouchdb)
- [vs Firebase: A Firebase alternative for local-first apps that keep their data](https://www.s3nd.sh/alternatives/firebase)
- [vs rclone: An rclone alternative for handing someone a code](https://www.s3nd.sh/alternatives/rclone)

## The ones that come up.
### What is s3nd?
A way to send anything with a code. s3nd put drops a file into an S3 bucket you own and prints eight characters; s3nd get on any other machine, with the code, downloads it. The same primitive runs inside your own app as a library and React hooks, and it carries structured data as well as files. [How it works](https://www.s3nd.sh/how-it-works)

### How is it different from WeTransfer, croc or Magic Wormhole?
The bytes go into your bucket and nowhere else: no hosted service, no relay, no account on either end. And unlike a live peer-to-peer transfer, the sender leaves and the code is redeemed later, until it expires. The comparison pages go tool by tool and say when the other one is the better choice. [Compare](https://www.s3nd.sh/alternatives)

### Do I need a server?
Not for machines you control: the CLI talks to the bucket directly with the credentials on that machine, and there is nothing to deploy. You need one as soon as a browser takes part, because a browser cannot hold S3 credentials. s3nd ships that server as one route file, and the CLI talks to it with a token instead of keys. [Without a server](https://doc.s3nd.sh/docs/no-server)

### Which storage providers work?
AWS S3 and anything that speaks the S3 API: Cloudflare R2, MinIO, Scaleway Object Storage, Wasabi, Ceph, Garage and the rest. Set an endpoint and s3nd switches the two defaults those providers expect. s3nd setup asks for what each provider needs, and s3nd doctor proves it works before you rely on it. [Storage providers](https://www.s3nd.sh/providers)

### Is a code secure?
A code is a bearer token: whoever has it can read that one transfer while it lives. The default is eight Crockford base32 characters, forty bits, which is sound when a transfer expires within a day and the lookup route is rate-limited. For sensitive payloads, encrypt before sending; s3nd stores whatever bytes you hand it. [Sync codes](https://doc.s3nd.sh/docs/sync-codes)

### How big can a file be?
From the CLI straight to the bucket, a file goes up in one PutObject, so it is bound by the memory of the sending machine rather than by a request limit: hundreds of megabytes are fine, multi-gigabyte archives wait for multipart, which is on the roadmap. Through your server, the runtime's request limit applies, 4.5 MB on Vercel functions and 6 MB on Lambda, unless you presign. [Limits](https://doc.s3nd.sh/docs/limits)

### What happens when a transfer expires?
It is never handed over again: the expiry is checked on every read, and an expired code answers the same NOT_FOUND as one that never existed. The object itself is deleted by a lifecycle rule on your bucket, which s3nd doctor checks you have, because a bucket quietly filling up with expired transfers is the most common way this goes wrong.

### Can it move an app's data, not only files?
Yes. A snapshot is structured state wrapped in an envelope with your app name, a schema version and an expiry, gzipped, and stored under a code. A local-first app exports its IndexedDB, posts it, and the user types the code on their other phone: no account, and the receiving build refuses a snapshot from a newer schema. That is where s3nd started. [Move an app to a new device](https://www.s3nd.sh/use-cases/new-device)

### What does it cost?
Nothing. Every package is MIT-licensed and there is no hosted service in the middle. The only bill is what your storage provider charges for a few objects that expire, and on Cloudflare R2 egress is free.

### Which runtimes does it run on?
The library and the CLI need Node 20 or later, because the AWS SDK does. The transfer handler takes a Request and returns a Response, so it drops into Next.js, Hono, Bun.serve, Deno or a worker without an adapter. The browser packages depend on fetch and nothing else, so they run in a browser, a worker or React Native.

