Attachments
Uploads require both a client feature flag and a server StorageAdapterFactory.
Client checks improve UX; server policy is authoritative.
Client
<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
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
uploadandresign. - Derive paths from the bound verified user; callers cannot choose another user's namespace.
- Return
nullfromresignfor 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.