# 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 request’s Session (the signed snapshot, at most 60 s old, or the database) and reads the membership from the database. Minting a download or upload URL, including a download redirect, also checks the database session. Purging the trash (`purge`, which deletes files for good) also checks the database session and is limited to Organization owners and admins; a Member gets HTTP 403. The SDK identifies presign, completion and proxy PUT as the same upload operation, so completion and proxy PUT each also read the database session once. URLs minted before revocation work until expiry, at most 15 minutes. 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.

## Files workspace [#files-workspace]

On the web, `useFilesWorkspace` (`features/files/files.hooks.ts`) owns the page: one tenant-scoped listing through the gateway, the Document Text status of each item and the selection the details dialog shows. Every Files query key starts with the Session identity (`user:organization`), so another Organization's same key never reuses a listing, a trash page or a signed URL. Components receive items and callbacks; the operations behind the card menu, the share dialog, the preview and the search live in `files.actions.hooks.ts`, `files.preview.hooks.ts` and `files.trash.hooks.ts`, and no component holds the files-sdk handle (W5).

The dashboard's file count and bytes come from the read-only `files.stats` procedure (`packages/api/src/features/files`), which sums the live objects under the Organization's prefix on the server and is prefetched by the dashboard loader. `uploaded_file` records confirmed uploads but not later deletes, moves or restores, so the listing, not the table, is the source.

## 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/bjj-travel/blob/main/apps/web/src/routes/api/files/$.ts)
- [`apps/web/src/features/files/files.gateway.server.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/apps/web/src/features/files/files.gateway.server.ts)
- [`packages/storage/src/create-r2-files.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/storage/src/create-r2-files.ts)
- [`packages/storage/src/upload-plugin.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/storage/src/upload-plugin.ts)
- [`apps/web/src/features/files/files.hooks.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/apps/web/src/features/files/files.hooks.ts)
- [`packages/api/src/features/files/files.service.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/api/src/features/files/files.service.ts)
- [`packages/api/src/features/uploads/uploads.service.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/api/src/features/uploads/uploads.service.ts)
- [`packages/storage/src/direct-s3.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/storage/src/direct-s3.ts)
- [`packages/storage/src/config.ts`](https://github.com/CarlosZiegler/bjj-travel/blob/main/packages/storage/src/config.ts)
- [`patches/files-sdk@2.6.2.patch`](https://github.com/CarlosZiegler/bjj-travel/blob/main/patches/files-sdk@2.6.2.patch)

