---
title: "Attachments"
description: "Enable uploads on both client and server, enforce MIME and size limits, and keep objects private."
---

Uploads require both a client feature flag and a server `StorageAdapterFactory`.
Client checks improve UX; server policy is authoritative.

## Client

```tsx
<ChatWidget
  apiBase="/api/chat"
  config={{
    schemaVersion: 1,
    runtime: { model: 'anthropic/claude-sonnet-4-5' },
    client: {
      features: {
        fileUpload: true,
        fileUploadAccept: 'image/*,application/pdf',
        fileUploadMaxBytes: 10 * 1024 * 1024,
      },
    },
  }}
/>
```

`fileUploadAccept` uses browser accept syntax. `fileUploadMaxBytes` prevents an
obviously oversized request but is not a security boundary.

## Server

```ts
createChatHandler({
  getUserId: getChatUserId,
  model,
  store,
  storage: createSupabaseStorage(),
  upload: {
    allowedMediaTypes: [
      'image/png',
      'image/jpeg',
      'image/webp',
      'application/pdf',
    ],
    maxBytes: 10 * 1024 * 1024,
  },
});
```

`allowedMediaTypes` is exact-match; `image/*` is not a server wildcard.
`maxBytes` defaults to 5 MiB and oversized files return `413`.

If no storage adapter is configured, the upload route returns `503` with
`File upload is not configured`. Hide the client upload affordance in that case.

## Security invariants

- Store objects in a private bucket.
- Return short-lived signed URLs from `upload` and `resign`.
- Derive paths from the bound verified user; callers cannot choose another
  user's namespace.
- Return `null` from `resign` for inaccessible/missing objects.
- Make removal idempotent.

The Supabase adapter uses
`<userId>/<conversationId>/<randomUUID>/<safeFilename>` and verifies the bound
user prefix for re-sign/removal. The service-role key must remain server-only.

History reload re-signs stored attachment paths. A missing object does not remove
the message or fail the full history response.
