Architecture
Un objet dans votre bucket, un code dans la main de quelqu’un.
s3nd est délibérément petit. Cette page en fait le tour complet : ce qu’est un transfert, ce qu’est un code, le protocole entre un serveur et ses clients, et pourquoi les packages sont découpés ainsi.
- Machine A
- s3nd put ./report.pdf
- ou POST /api/transfers depuis votre app
- → K7QP2M4X
- Votre bucket
- drop/K7QP2M4X
- 284 ko · expire dans 1 h
- S3, R2, MinIO, Scaleway, Wasabi
- Machine B
- s3nd get k7qp-2m4x
- ou GET /api/transfers/:code/raw
- → report.pdf, puis rm
01Un transfert
Un objet, un code, une expiration.
Un fichier est stocké tel quel, avec le nom, le type de contenu et l’expiration dans les métadonnées de l’objet. Des données structurées sont stockées comme un snapshot : une enveloppe qui se décrit elle-même, gzippée. Les deux vivent sous le code.
drop/K7QP2M4X
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
x-amz-meta-s3nd-kind: file
x-amz-meta-s3nd-expires-at: 2026-08-27T13:00:00.000Z
Body: the bytes, untouched{
"s3nd": 1, // envelope format, not your data's
"app": "notes",
"version": 3, // your schema version
"device": "Pixel 8",
"createdAt": "2026-08-27T12:00:00.000Z",
"expiresAt": "2026-08-27T13:00:00.000Z",
"data": { /* whatever you passed */ }
} // gzipped, typically 5–10× smallerUn transfert expiré n’est jamais remis, même si l’objet est encore dans le bucket. Un code expiré répond le même NOT_FOUND qu’un code qui n’a jamais existé, donc personne ne peut sonder quels codes ont servi.
Supprimer l’objet est le travail de votre bucket, via une règle de cycle de vie sur le préfixe. s3nd doctor vérifie que vous en avez une, parce qu’un bucket qui se remplit discrètement de transferts expirés est la façon la plus courante de se tromper.
Un snapshot porte votre version de schéma. Passez maxVersion à la lecture et un snapshot issu d’une version plus récente lève SNAPSHOT_TOO_NEW au lieu d’atterrir dans une app qui va le mal lire.
02Codes de synchronisation
Quarante bits qui survivent à un appel téléphonique.
Un code, c’est toute l’expérience utilisateur d’un transfert. Il apparaît sur un écran, quelqu’un le tape sur un autre, et tout dans le code découle de là.
Base32 Crockford
Ni I, ni L, ni O, ni U. Les trois premiers sont ceux que l’on confond ; retirer le quatrième évite qu’un code aléatoire n’épelle quelque chose de malheureux.
Normalisé au retour
Séparateurs retirés, casse repliée, et O lu comme zéro uniquement quand il n’y a aucune lettre O avec laquelle le confondre. La réparation se fait dans le navigateur, avant toute requête.
Réservé par une écriture conditionnelle
Le serveur choisit le code et écrit avec ifAbsent, donc une collision échoue bruyamment et réessaie avec un code neuf au lieu d’écraser le transfert d’un inconnu.
03Le protocole
Quatre routes, un format d’erreur. Écrits noir sur blanc.
Un navigateur ne peut pas détenir vos identifiants S3, donc dès qu’il participe, un serveur se place au milieu. La forme de ce milieu est un protocole, pas ce que le handler fait par hasard.
POST / # create a transfer, get the code back
GET /:code # metadata; the state inline for a snapshot
GET /:code/raw # the bytes, or a 302 to a presigned URL
DELETE /:code # burn it
# every error, same shape
{ "error": { "code": "NOT_FOUND", "message": "Unknown or expired code." } }import { createTransferClient } from '@s3nd/protocol'
const transfers = createTransferClient({ baseUrl: '/api/transfers' })
const { code } = await transfers.createFile({ body: file, filename: file.name })
const meta = await transfers.read(typed) // null when unknown or expired
const bytes = await transfers.readBytes(code) // the file back
await transfers.remove(code)Un client fonctionne avec n’importe quel serveur qui répond à ces routes, pas seulement avec createTransferHandler(). Un serveur en Go ou en Rails fonctionne avec tous les clients s3nd. Et la CLI pointée sur --remote ne peut pas savoir à qui elle parle, ce qui est exactement pourquoi s3nd put fonctionne contre votre propre déploiement.
Ce que vous envoyez décide de ce que contient un transfert : un corps JSON est un snapshot, tout autre type de contenu est un fichier avec son nom dans X-S3nd-Filename. Les clients lèvent une TransferError qui porte le code d’erreur ; branchez sur le code, jamais sur le message.
04Deux écrivains
Quand deux machines écrivent, « le dernier gagne » est une perte de données.
Un transfert unique a un seul écrivain. Une sauvegarde par utilisateur en a deux, et le comportement par défaut de S3 garde silencieusement celui qui est arrivé en dernier.
// Claim a fresh code: write only if nothing sits under it.
await store.putSnapshot(code, state, { ifAbsent: true })
// Rewrite a shared backup: fail if someone wrote since you read.
const current = await store.getSnapshot(`user-${userId}`)
await store.putSnapshot(`user-${userId}`, merged, { ifMatch: current?.etag })
// → PRECONDITION_FAILED when another device won. Read again, merge again.Les deux options sont de simples en-têtes conditionnels S3. ifAbsent est la façon de réserver un code neuf ; ifMatch est la façon dont un second appareil apprend qu’il a perdu la course. Elles fonctionnent chez tous les fournisseurs qui les implémentent, et l’exemple Node est là pour vérifier que le vôtre en fait partie.
05Les packages
Une seule contrainte décide du découpage.
Un navigateur ne doit jamais se retrouver avec un client de stockage dans son arbre de dépendances. Le package protocol est ce que les deux moitiés partagent, et c’est la seule raison de son existence.
@s3nd/cli
s3ndput, get, rm, doctor, init et config. Une seule implémentation, le client du protocole, branchée soit sur fetch, soit directement sur le handler dans le même processus.
s3nd
aws-sdk, protocolFichiers, snapshots, écritures conditionnelles et le handler de transfert. Le seul package qui détient des identifiants, donc le seul qui tourne sur un serveur.
@s3nd/protocol
nanoidLe format sur le fil, un client basé sur fetch, et les codes de synchronisation. Rien ici n’importe de client de stockage, et c’est ce qui permet à un navigateur de le partager.
@s3nd/react
protocol, react (peer)Envoyer, recevoir, et un champ de code. Dépend du protocole et jamais de S3, donc aucun chemin de votre bundle n’atteint le SDK AWS.
- Runtime serveur
- Node 20 ou plus, ce que le SDK AWS v3 exige
- Handler
- Request en entrée, Response en sortie : Next.js, Hono, Bun.serve, Deno, workers
- Packages navigateur
- fetch et rien d’autre : navigateur, worker, React Native, Deno
- Tests
- Hors ligne, contre un S3 en mémoire qui honore les en-têtes conditionnels
Déposez un fichier. Donnez le code.
Pointez-le sur le bucket que vous payez déjà. Rien à déployer, aucune inscription, personne au milieu.