> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# File uploads

> Let users upload files (avatars, attachments, generated images) to R2.

DeepSpace apps get a per-app R2 bucket out of the box. The `useR2Files` hook covers uploads, listings, deletions, and authenticated downloads - all proxied through the platform-worker so the app never holds R2 credentials directly.

This guide shows the common end-to-end flows. For the full method signatures and types, see the [files reference](/sdk-reference/client/files).

## How files are wired

You don't add an R2 binding yourself. The starter worker already proxies `/api/files/*` to the platform-worker, which holds a shared bucket and namespaces keys per app (via the `APP_NAME` the worker forwards on each request). Every write is gated by the caller's signed JWT.

The client side is a single hook:

```ts theme={null}
import { useR2Files } from 'deepspace'
```

## Upload from a file input

The most common case - an `<input type="file">` or drag-drop event. Pass the resulting `File` to `upload`:

```tsx theme={null}
import { useR2Files } from 'deepspace'

function FileUploader() {
  const { upload, isUploading } = useR2Files()

  async function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return
    const result = await upload(file, file.name)
    if (result.success) {
      console.log('uploaded:', result.key)
    } else {
      console.error(result.error)
    }
  }

  return (
    <input type="file" onChange={onFileChange} disabled={isUploading} />
  )
}
```

Always check `result.success` before reading `result.key` - the upload may fail (network, auth). `isUploading` is true while a request is in flight.

## Upload generated data (canvas, cropped image)

When you have data as a Base64 string - for example, from `<canvas>.toDataURL()` - use `uploadBase64`. The display name is required so the file has an `originalName` for later downloads:

```tsx theme={null}
const { uploadBase64 } = useR2Files()

async function saveCanvasAsImage(canvas: HTMLCanvasElement) {
  const dataUrl = canvas.toDataURL('image/png')
  const base64 = dataUrl.split(',')[1]
  const result = await uploadBase64(base64, 'drawing.png', 'image/png')
  if (!result.success) console.error(result.error)
}
```

## List and render a user's files

`list()` is an async function - call it and store the result in state rather than reading a reactive array:

```tsx theme={null}
import { useState, useEffect } from 'react'
import { useR2Files, formatFileSize } from 'deepspace'
import type { R2FileInfo } from 'deepspace'

function Gallery() {
  const { deleteFile, list, getUrl } = useR2Files()
  const [files, setFiles] = useState<R2FileInfo[]>([])

  async function refresh() {
    setFiles(await list())
  }

  useEffect(() => { refresh() }, [])

  return (
    <div>
      {files.map((f) => (
        <div key={f.key}>
          <img src={getUrl(f)} alt="" />
          <p>{f.originalName ?? f.key} - {formatFileSize(f.size)}</p>
          <button onClick={async () => { await deleteFile(f); refresh() }}>
            Delete
          </button>
        </div>
      ))}
    </div>
  )
}
```

Re-call `list()` after mutations - there's no reactive cache. `formatFileSize` and `isImageFile` are display helpers exported from `deepspace`.

## Authenticated downloads

`getUrl(fileOrKey)` returns a plain URL with no auth token attached. It works for unauthenticated reads on deployed sites (the platform-worker resolves the app from `APP_NAME` and serves reads without a JWT), which is what you want for `<img src>`. For everything else, use `downloadFile` or `readFile`:

```ts theme={null}
const { downloadFile, readFile } = useR2Files()

// Trigger a Save As… dialog. Uses originalName as the filename automatically.
const result = await downloadFile(file)
if (!result.success) console.error(result.error)

// Or read the bytes yourself - returns a Response you can .text(), .blob(), etc.
const resp = await readFile(file)
const text = await resp.text()
```

Both accept either an `R2FileInfo` from `list()` or a raw key string.

## Storing metadata (MIME type, captions, tags)

`R2FileInfo` carries `key`, `size`, `uploaded`, `url`, `originalName`, and `uploadedBy` - and nothing else. There's no `mimeType` field. For richer metadata, store a sidecar record in a [collection](/concepts/data-model):

```ts theme={null}
// src/schemas/attachments-schema.ts
import type { CollectionSchema } from 'deepspace'

export const attachmentsSchema: CollectionSchema = {
  name: 'attachments',
  columns: [
    { name: 'fileKey', storage: 'text', interpretation: 'plain' },
    { name: 'mimeType', storage: 'text', interpretation: 'plain' },
    { name: 'caption', storage: 'text', interpretation: 'plain' },
  ],
  permissions: {
    member: { read: true, create: true, update: 'own', delete: 'own' },
  },
}
```

Create the sidecar alongside the upload. The snippet assumes a [`RecordProvider`](/sdk-reference/client/records) higher in the tree that has registered the `attachments` collection - `useMutations` throws otherwise.

```tsx theme={null}
import { useR2Files, useMutations } from 'deepspace'

type Attachment = { fileKey: string; mimeType: string; caption: string }

const { upload } = useR2Files()
const { create } = useMutations<Attachment>('attachments')

async function uploadWithMeta(file: File) {
  const result = await upload(file, file.name)
  if (!result.success || !result.key) {
    console.error(result.error)
    return
  }
  await create({
    fileKey: result.key,
    mimeType: file.type,
    caption: '',
  })
}
```

## Scoping and permissions

`useR2Files` is always scoped to the current app - the platform derives the bucket prefix from `APP_NAME`. There's no per-user or cross-app scope exposed by the hook. If you need finer namespacing (per-room, per-project), encode it into the `name` argument at upload time or store it on a sidecar record.

Writes require a signed user JWT. Anyone who knows a file's key and hits your app's `/api/files/<key>` can read it; if a file should be private, gate access in your UI and use `readFile`/`downloadFile` rather than rendering its URL. There is no recycle bin - `deleteFile` is immediate and irreversible.

## Local development

<Warning>
  R2 uploads require an `APP_IDENTITY_TOKEN` minted by the deploy worker. Until the app has been deployed at least once, the CLI can't fetch one, so `upload()` round-trips return 401 from the platform file gateway. After a first deploy, `npx deepspace dev` provisions the token into `.dev.vars` and uploads work locally.
</Warning>

For tests written before the first deploy, assert that uploads are *dispatched* (the function is called) rather than asserting on the round-trip. Or run `npx deepspace dev --prod` to point local UI at production workers.

## Next steps

* [Files reference](/sdk-reference/client/files) - full method signatures, return types, and the `R2FileInfo` shape.
* [Custom bindings](/guides/custom-bindings) - declare a wholly separate R2 bucket with custom permissions.
* [Data model](/concepts/data-model) - pair files with sidecar records for queryable metadata.
