# Files and uploads

Send files directly to R2, confirm durable upload intent and recover interrupted multipart uploads.

<Diagram name="uploads" alt="An authenticated upload persists intent, creates an immutable R2 object, then confirms metadata; cron recovers unfinished confirmation." />

## Gateway and isolation [#gateway-and-isolation]

The web client uses authorized upload procedures for direct byte transport and the files-sdk gateway at `/api/files` for browsing, downloads and deletion. Every operation checks the current Session and membership. The server applies `orgs/<organizationId>/`; a filename cannot select or escape an Organization. One private R2 bucket serves all Organizations.

Keys preserve the User's filename, including spaces and Unicode, under [ADR-0001](/docs/adr/0001-stored-file-keys-keep-the-user-s-filename). Treat keys as opaque and encode them for URLs. Do not reintroduce filename sanitization that silently changes the display name.

## Durable upload lifecycle [#durable-upload-lifecycle]

`uploads.prepare` persists an intent after validating membership, MIME type and size. A signed PUT binds the exact key, size, content type and conditional create. Files above 16 MiB use multipart uploads with 8 MiB parts, at most three concurrent parts and at most three attempts per part. The browser sends `File.slice()` objects and resumes from server-listed parts.

Completion validates part lengths and ETags, then checks HEAD metadata before confirming the file. Signed URLs expire after 15 minutes; intents last 24 hours. Cron recovers missing acknowledgements and aborts abandoned multipart sessions. Explicit cancellation aborts multipart; single-PUT cleanup waits 16 minutes to cover a late upload through an issued URL.

Images can be up to 100 MiB, documents 1 GiB and ZIP archives 5 GiB. The legacy gateway proxy remains capped at 10 MiB and reauthorizes PUTs through the files-sdk patch. Soft delete is enabled; versioning is not. Text previews stop at 64 KiB, and downloads can use range requests without loading the whole original into the Worker.

Confirmed supported files enter the separate [Document Text workflow](/docs/features/documents). Storage limits and conversion limits are independent; a stored original can remain downloadable when text preparation fails. [ADR-0008](/docs/adr/0008-documents-and-large-uploads) explains both paths.

## Configure and extend [#configure-and-extend]

Supply `STORAGE_BUCKET`, `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY_ID` and `STORAGE_SECRET_ACCESS_KEY` in the intended environment. Setup does not create an R2 bucket. Configure bucket CORS for the actual web origins, upload/read methods and signed headers; expose `ETag` so multipart completion can read it. `apps/web/r2-cors.json` is the starting template, not a list of every worktree or deployed origin.

Extend size/MIME rules in storage config and retain admission, confirmation and source-version checks. Keep `files-sdk` on one compatible copy across web, API and storage and rebase its pinned patch on upgrades. Verify upload, download byte equality, anonymous denial, prefix isolation, multipart resume/cancel and interrupted confirmation.


## Key files [#key-files]

- [`apps/web/src/routes/api/files/$.ts`](https://github.com/CarlosZiegler/doro/blob/main/apps/web/src/routes/api/files/$.ts)
- [`packages/storage/src/create-r2-files.ts`](https://github.com/CarlosZiegler/doro/blob/main/packages/storage/src/create-r2-files.ts)
- [`packages/storage/src/upload-plugin.ts`](https://github.com/CarlosZiegler/doro/blob/main/packages/storage/src/upload-plugin.ts)
- [`packages/api/src/uploads/workflow.ts`](https://github.com/CarlosZiegler/doro/blob/main/packages/api/src/uploads/workflow.ts)
- [`packages/storage/src/direct-s3.ts`](https://github.com/CarlosZiegler/doro/blob/main/packages/storage/src/direct-s3.ts)
- [`packages/storage/src/config.ts`](https://github.com/CarlosZiegler/doro/blob/main/packages/storage/src/config.ts)
- [`patches/files-sdk@2.6.2.patch`](https://github.com/CarlosZiegler/doro/blob/main/patches/files-sdk@2.6.2.patch)

