Quick start
Four ways in, depending on what you are building. Pick yours; each one gets you to a working code in a few minutes.
s3nd is four packages, and the right one depends on where the transfer starts. Pick the line that sounds like what you are building:
Move files from a terminal
Between your own machines, or out of CI. One global install, nothing to deploy. @s3nd/cli
Send and receive in a React app
A button that hands out a code, a field that takes one. @s3nd/react in the browser, @s3nd/core on your server.
Write snapshots from server code
Your own routes call the bucket directly: Next.js, Hono, Express, a job. @s3nd/core
Talk to a s3nd server from elsewhere
React Native, a worker, a script, another framework. @s3nd/protocol: fetch and nothing else.
| You are building | Install | Runs on | Needs a server |
|---|---|---|---|
| A terminal workflow, CI | npm install -g @s3nd/cli | your machines | no |
| A React app | @s3nd/react + @s3nd/core | browser + your server | yes, yours |
| Server-side code | @s3nd/core | Node 20+, Bun, Deno, workers | it is one |
| A client in another runtime | @s3nd/protocol | anything with fetch | yes, any s3nd |
The first three need a bucket you can write to: AWS S3, Cloudflare R2, MinIO, Scaleway, Wasabi or any other S3-compatible storage. Providers says what each one wants. The last one only needs the URL of a server that already has one.
From a terminal
Install the CLI
npm install -g @s3nd/cliAnswer a few questions
s3nd setupIt asks where transfers are stored, the bucket and the keys, with where to find each one. The
answers go to ~/.config/s3nd/config.json, readable by you only and used from any directory,
and it ends by writing, reading and deleting a probe object to prove the bucket works.
Skip this step if you like: the first command that needs a bucket offers to run it.
Send, then receive
s3nd put ./report.pdf # prints a code: K7QP2M4Xs3nd get k7qp-2m4x # on the other machine; case and dashes do not matterNext: the CLI reference, or Without a server for the whole R2 setup, CI included.
In a React app
A browser cannot hold your bucket's keys, so this path has two halves: four routes on your server, and hooks in the browser that talk to them. The AWS SDK never reaches the client bundle.
Install both halves
npm install @s3nd/core @s3nd/reactMount the routes on your server
import { createBucket, createTransferHandler } from '@s3nd/core'
export const { GET, POST, DELETE } = createTransferHandler({
bucket: createBucket({ bucket: process.env.S3_BUCKET, region: process.env.AWS_REGION }),
app: 'notes',
expiresIn: 3600,
})The handler takes a Request and returns a Response, so the same line works in Hono, Bun or a
worker. Every route is public until you pass authorize: Setting up a server.
Point the provider at them
'use client'
import { S3ndProvider } from '@s3nd/react'
export function Providers({ children }: { children: React.ReactNode }) {
return <S3ndProvider baseUrl="/api/transfers">{children}</S3ndProvider>
}Send on one device, receive on the other
import { useSendTransfer } from '@s3nd/react'
export function MoveToAnotherDevice() {
const { send, transfer, isPending } = useSendTransfer()
return (
<>
<button onClick={() => send(exportState())} disabled={isPending}>
Move to another device
</button>
{transfer && <p>Type this on the other device: {transfer.code}</p>}
</>
)
}exportState and importState are yours: only your app knows its own storage. The
IndexedDB example
has a complete pair.
Next: React for every hook and option, The transfer protocol for what the routes do.
From your server
When your own routes decide what gets stored, call the bucket directly. No protocol, no client package: a code, a snapshot, and your own two endpoints.
Install the core
npm install @s3nd/coreCredentials come from the options, the usual AWS_* variables, or the platform's role:
Installation.
Create the bucket once
import { createBucket } from '@s3nd/core'
export const store = createBucket({
bucket: process.env.S3_BUCKET,
region: process.env.AWS_REGION,
prefix: 'snapshots',
maxSize: 4 * 1024 * 1024,
})
export const SCHEMA_VERSION = 3createBucket() is cheap, since the S3Client is built on the first request, so module scope is
fine. maxSize below your platform's request limit turns an oversized snapshot into a clean 413.
Store a snapshot under a fresh code
import { store, SCHEMA_VERSION } from '@/lib/store'
export async function POST(request: Request) {
const code = store.codes.create()
await store.putSnapshot(code, await request.json(), {
app: 'notes',
version: SCHEMA_VERSION,
expiresIn: 60 * 60,
ifAbsent: true,
})
return Response.json({ code })
}expiresIn keeps a code from outliving the transfer: it is a bearer token, and a short life is
most of its security. ifAbsent makes a collision fail loudly instead of overwriting a stranger's
snapshot.
Hand it back on the other device
import { store, SCHEMA_VERSION } from '@/lib/store'
export async function GET(_request: Request, { params }: { params: Promise<{ code: string }> }) {
const { code } = await params
const snapshot = await store.getSnapshot(store.codes.normalize(code), { maxVersion: SCHEMA_VERSION })
if (!snapshot) return Response.json({ error: 'Unknown or expired code' }, { status: 404 })
return Response.json({ data: snapshot.data, createdAt: snapshot.createdAt })
}store.codes.normalize() is what makes a typed code work: separators dropped, case folded, the
characters people misread repaired. Call it on user input, always. maxVersion makes a snapshot
from a newer build throw rather than land in an app that would misread it.
Next: Snapshots for what is in the envelope, Sync codes for how guessable a code really is, Two devices, one snapshot for when both sides write.
From another runtime
Anything with fetch can talk to a s3nd server, whether createTransferHandler() runs it or
something written in Go does:
npm install @s3nd/protocolimport { createTransferClient } from '@s3nd/protocol'
const transfers = createTransferClient({ baseUrl: 'https://drop.example.com/api/transfers' })
const { code } = await transfers.createSnapshot({ data: state, version: 3 })
const incoming = await transfers.read(typedCode) // null when unknown or expiredIts whole dependency tree is nanoid; no path from it reaches the AWS SDK, so it bundles for React Native or a worker as is. Next: The transfer protocol.