Skip to main content

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.

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.

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:
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:
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:
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:
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:
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:
// 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 higher in the tree that has registered the attachments collection - useMutations throws otherwise.
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

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.
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 - full method signatures, return types, and the R2FileInfo shape.
  • Custom bindings - declare a wholly separate R2 bucket with custom permissions.
  • Data model - pair files with sidecar records for queryable metadata.