Core concepts
Uploading files
How uploads work end to end: direct-to-R2 presigned PUTs, size limits per tier, content-type handling, integrity hashing, and finalizing a file record.
The upload model
Bytes go straight from the browser to Cloudflare R2 over a presigned PUT. The app server only brokers the slot and finalizes the record — it never streams your file. This keeps uploads fast, cheap, and able to handle multi-gigabyte files without server memory pressure.
- Presign —
POST /api/upload/presignvalidates size, type, and visibility, creates apendingfile row, and returns a short-lived upload URL. - Put — the browser uploads directly to the returned URL with the exact headers.
- Complete —
POST /api/upload/completeHEADs the object to confirm it exists at the expected size, then activates the file.
Note
udrive/<yyyy>/<mm>/<nanoid>/<name> — so uploads can never overwrite each other or be guessed by path.Limits & quotas
| Tier | Max file size | Storage quota | Visibility options |
|---|---|---|---|
| Anonymous | 100 MB | Per-IP daily cap | public, unlisted |
| Wallet connected | 2 GB | 2 GB total | public, unlisted, private |
The presign call rejects oversize files, over-quota uploads, and invalid content types before any storage slot is issued. A successful upload increments your storageUsed on complete; deleting a file frees it again.
Names & content types
Filenames are sanitized server-side: path separators, control characters, leading dots, and Windows-reserved characters are stripped, and overly long names are truncated while preserving the extension. The original (sanitized) name is what download recipients see.
Provide an accurate contentType; it is stored and used to set the download's Content-Type and to decide which files are safe to preview inline. Downloads are always served as attachments with X-Content-Type-Options: nosniff.
Integrity hashing
You can pass a sha256 hex digest to /api/upload/complete. It is stored alongside the file and becomes part of the provenance statement, so a later byte change invalidates any signature.
const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf);
const sha256 = [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");File lifecycle
Every file moves through a small set of statuses:
| Status | Meaning |
|---|---|
pending | Presigned, bytes not yet confirmed. Not downloadable. |
active | Finalized and serveable per its visibility. |
quarantined | Hidden after abuse reports cross a threshold; serves a 404. |
deleted | Soft-deleted; object removed from R2. |
Tip
expiresAt on upload or later via PATCH /api/files/:id to make a file self-destruct.