Skip to content
Docs
Core platformUpdated 2026-08-20

File storage and R2

Store private user files in R2 while keeping ownership and metadata in D1.

Storage model

R2 stores bytes. D1 stores file identity, owner, display name, content type, size, storage key, and timestamps.

Every storage key is user-namespaced. Every query includes the current user ID.

User workflow

Authenticated users can:

  1. upload a file;
  2. list their files;
  3. download through an ownership-checked route;
  4. delete both the R2 object and D1 metadata.

Downloads are streamed through /api/files/:id instead of exposing a public bucket URL.

Server operations

The file domain wraps the narrow storage contract with validation and persistence:

ts
await saveUserFile(db, session.user.id, file)
await getUserFile(db, session.user.id, fileId)
await deleteUserFile(db, session.user.id, fileId)

Safety rules

  • Enforce maximum size before writing the object.
  • Normalize unsafe filenames for metadata and response headers.
  • Never accept a client-provided storage key.
  • Verify ownership on list, get, and delete.
  • Remove user objects before deleting an account.

Add another storage provider

Implement the narrow put, get, delete, and list contract, then register the adapter. Product routes should remain unchanged.

TanStarter SES documentationBuilt with TanStack Start and Content Collections.