# How it works

> A parcel is a file in your bucket under an eight-character code, with a lifetime and a delivery receipt beside it: sent, opened, approved or changes. The pickup page, the receipt, large files, and the protocol underneath.

Canonical: https://colis-site.vercel.app/en/how-it-works · Markdown: https://colis-site.vercel.app/en/how-it-works.md · Français: https://colis-site.vercel.app/how-it-works

colis is deliberately small. This page is the whole of it: what a parcel is, what the receipt records, how large files travel, and the protocol the page, the CLI and the automations share.

**You. Your bucket. Your client. Nothing else.**

- **You send** — The file lands in your bucket as one object under a fresh eight-character code, with its expiry stamped on the object and a receipt beside it.
- **The code changes hands** — A link in an email, eight characters read out on a call, a QR code on a screen. No account on either side.
- **Your client answers** — They type the code, typos repaired, preview the file and approve it. The receipt records it, and the webhook goes out.

## Sent, opened, approved or changes.
Every parcel gets a receipt, kept beside it in your bucket: four states, each with its time, and one answer that never changes.

- **envoyé · sent** — Written when the parcel is created, with a hash of your sender token. That token is how your own visits are told apart from your client’s.
- **ouvert · opened** — The first time someone other than you opens the pickup page, downloads the file or answers. Only the first time.
- **validé · approved** — The client pressed approve. Final: a second answer is refused.
- **à corriger · changes** — The client asked for changes and said which, in up to 1,000 characters. Final too: the next version is a new parcel.

**the receipt, from a terminal**
```sh
$ colis statut K7QP2M4X
à corriger
envoyé      2026-09-22 10:00
ouvert      2026-09-22 11:30
à corriger  2026-09-22 12:45
comment     Le logo en SVG, et le fond plus clair.

$ colis statut K7QP2M4X --json    # the whole receipt, for a script
```

The receipt outlives the file: it still answers after a one-time download burned the code, for as long as the deployment’s longest lifetime. Your sending page follows it on its own, `colis statut` prints it, and each change is a webhook.

## One object, one code, one lifetime.
The file is stored as the bytes you gave, with its name, its type and its expiry in the object’s metadata. Everything else the page knows sits in small records beside it, under `meta/`, out of reach of the transfer routes.

**a parcel, as it lands in the bucket**
```text
drop/K7QP2M4X                 # the file
  Content-Type:         application/pdf
  Content-Disposition:  attachment; filename="maquette-v2.pdf"
  x-amz-meta-colis-kind:        file
  x-amz-meta-colis-expires-at:  2026-09-23T10:00:00.000Z
  Body:                 the bytes, untouched

drop/meta/K7QP2M4X            # note, device, one-time flag, password hash
drop/meta/K7QP2M4X.delivery   # sent, and the sender's token hash
drop/meta/K7QP2M4X.opened     # the first time the client opened it
drop/meta/K7QP2M4X.verdict    # approved, or changes and the comment
```

- **expiry on read** — A parcel past its expiry is never handed over, even if the object is still in the bucket. An expired code answers the same `NOT_FOUND` as one that never existed.
- **lifecycle rule** — Deleting the object is your bucket’s job, through a lifecycle rule on the prefix. `colis verifier` checks you have one.
- **password** — A parcel’s password is hashed with scrypt under its own salt. Only the hash is stored, so nobody can read it back or reset it.
- [How big a file can be](https://colis-docs.vercel.app/docs/limites)
- [The delivery page, in full](https://github.com/mamadouwhile/colis/blob/main/templates/drop/README.md)

## Straight from the browser to your bucket.
Above `DROP_MAX_SIZE_MB`, nothing goes through the function. The server signs URLs and assembles; the browser sends the parts itself.

```text
POST   /api/transfers/uploads                 # a fresh code, a multipart upload
       → { code, uploadId, partSize, partCount }   # parts of 8 MiB
POST   /api/transfers/uploads/:code/parts     # presigned PUT URLs, 50 at most
PUT    <the bucket>                           # the browser, 4 parts at a time
POST   /api/transfers/uploads/:code/complete  # assembled, checked, then 201
DELETE /api/transfers/uploads/:code           # abandoned
```

Parts are 8 MiB, larger when a file would need more than 10,000 of them. The browser sends four at a time and retries a failed one three times.

It also remembers the upload. Drop the same file again after a lost connection or a closed tab, and it asks the bucket which parts are already there and sends the rest.

The bucket needs a CORS rule that allows `PUT` from your page and exposes `ETag`. Without it, small files keep working and a large one fails with a message that says why.

## Forty bits that survive a phone call.
The code is what your client handles. It appears on your screen and they type it into theirs, and everything about it is shaped by that.

- **Crockford base32** — No `I`, `L`, `O` or `U`. The first three are what people misread; dropping the fourth keeps a random code from spelling something unfortunate.
- **Repaired on the way back** — Separators dropped, case folded, and `O` read as zero. The repair happens in the browser, before any request, and what the client typed stays as typed.
- **Claimed with a conditional write** — The server picks the code and writes with `ifAbsent`, so a collision fails loudly and retries with a fresh code instead of overwriting someone else’s parcel.
- [Sync codes](https://colis-docs.vercel.app/docs/protocole#les-codes)
- [Length, alphabet, and what each costs](https://colis-docs.vercel.app/docs/configuration#la-forme-des-codes)

## Four routes, one error format. Written down.
A browser cannot hold your S3 credentials, so a server sits in the middle. The delivery page is that server, and its shape is a protocol, not whatever the handler happens to do.

**relative to /api/transfers**
```text
POST   /                # create a transfer, get the code back
GET    /:code           # metadata
GET    /:code/raw       # the bytes, or a 302 to a presigned URL
DELETE /:code           # burn it

# what the delivery page adds
GET    /:code/status    # sent, opened, approved or changes
POST   /:code/opened    # the client opened it
POST   /:code/verdict   # { "decision": "approved" }, or "changes" and a comment

# every error, same shape
{ "error": { "code": "NOT_FOUND", "message": "Unknown or expired code." } }
```
**the client, in a browser**
```ts
import { createTransferClient } from '@colis/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)
```

The CLI pointed at `--remote` speaks the same four routes, which is why `colis envoyer` works against your delivery page with a token instead of S3 keys.

The delivery page adds three routes of its own for the receipt. A client that knows none of them still works: what it creates is a parcel like any other, with the deployment’s defaults.

[The transfer protocol, route by route](https://colis-docs.vercel.app/docs/protocole)

## One constraint decides the split.
A browser must never end up with a storage client in its dependency tree. The protocol package is what both halves share.

- **@mdemb/colis** (The binary; core, protocol) — envoyer, recevoir, statut, annuler, verifier, init and config. One implementation, the protocol client, wired to fetch or straight into the handler in-process.
- **@colis/core** (The primitive; aws-sdk, protocol) — Files, records, conditional writes and the transfer handler. The only package that holds credentials.
- **@colis/protocol** (The contract; nanoid) — The wire format, a fetch-based client, the codes, and the webhook signature: signWebhook and verifyWebhook.
- **@colis/react** (The hooks; protocol, react (peer)) — Send, receive, and a code input. No path from your bundle reaches the AWS SDK.
- **n8n-nodes-colis** (The n8n node; protocol, n8n-workflow (peer)) — A trigger that checks the signature, and an action node that sends, reads and burns parcels.

- **Server runtime**: Node 20 or later, what the AWS SDK v3 requires
- **Delivery page**: Next.js, deployed on Vercel from the monorepo
- **Browser packages**: fetch and nothing else
- **On npm**: Not yet: built from the repository
