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/presign validates size, type, and visibility, creates a pending file 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/complete HEADs the object to confirm it exists at the expected size, then activates the file.

Note

Object keys are derived from a random id — udrive/<yyyy>/<mm>/<nanoid>/<name> — so uploads can never overwrite each other or be guessed by path.

Limits & quotas

TierMax file sizeStorage quotaVisibility options
Anonymous100 MBPer-IP daily cappublic, unlisted
Wallet connected2 GB2 GB totalpublic, 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.

TypeScript
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:

StatusMeaning
pendingPresigned, bytes not yet confirmed. Not downloadable.
activeFinalized and serveable per its visibility.
quarantinedHidden after abuse reports cross a threshold; serves a 404.
deletedSoft-deleted; object removed from R2.

Tip

Owners can set an expiresAt on upload or later via PATCH /api/files/:id to make a file self-destruct.