Feature: Support File Uploads up to 5 MB
Goal
Add end-to-end support for uploading files to a conversation.
Users should be able to attach files from the UI, send them as part of the conversation flow, and have the backend persist and expose the required file information.
The maximum supported file size should be 5 MB per file.
This feature must be implemented across the full stack:
UI
↓
API / Server
↓
File processing
↓
Persistence
↓
Conversation / Message
The limit must not exist only in the UI. It must be enforced consistently by the backend as well.
UI
The chat composer should allow the user to attach a file before sending a message.
The UI should support:
- Selecting a file from the user's device.
- Showing the selected file before sending.
- Displaying the file name and size.
- Allowing the user to remove the attachment before sending.
- Showing an upload/loading state when required.
- Displaying attached files as part of the message after sending.
- Re-displaying attachments when loading an existing conversation from Chat History.
Client-side Validation
The UI should reject files larger than 5 MB before attempting the upload.
For example:
File "large-report.pdf" exceeds the maximum allowed size of 5 MB.
This is only a UX validation.
The backend must independently enforce the same limit.
Backend / API
The server should support receiving file attachments as part of the conversation/message flow.
Conceptually:
User selects file
↓
Upload file
↓
Server validates file
↓
Persist attachment
↓
Associate attachment with message/conversation
↓
Continue agent execution
The backend must validate:
- File exists.
- File size is not greater than 5 MB.
- File metadata is valid.
- The upload belongs to the correct conversation/user context.
A request containing a file larger than 5 MB must be rejected by the server even if the client-side validation was bypassed.
For example:
with a clear response:
{
"error": "file_too_large",
"message": "File size exceeds the maximum allowed size of 5 MB."
}
Server Configuration
The 5 MB limit should be supported at every server layer involved in handling the request.
This includes checking limits configured in:
- Web framework.
- Multipart handling.
- Request body limits.
- Reverse proxy / gateway, if applicable.
- Application-level validation.
We should avoid a situation where the UI allows 5 MB but another layer rejects the request at a smaller size.
The limit should preferably be defined from a single configuration value or constant.
Generic File Storage Architecture
The file implementation must be storage-agnostic.
We should not couple the upload feature directly to the current persistence mechanism.
For the initial implementation, the file content may be stored in the database if that fits the current architecture. However, the application layer should not depend on files being stored in the DB.
In the future, we may want to store file content in:
- S3.
- Another object storage provider.
- A filesystem.
- A dedicated file service.
- Another persistence mechanism.
Changing the storage backend should not require changing the conversation, message, API, or UI layers.
The architecture should therefore introduce a generic file storage abstraction.
For example:
FileStorage
│
┌─────────┼─────────┐
↓ ↓ ↓
Database S3 Other Storage
Conceptually:
FileStorage
├── store(...)
├── get(...)
├── delete(...)
└── ...
Higher-level components should depend only on this abstraction:
Conversation / Message Service
↓
Attachment Service
↓
FileStorage
↓
Implementation
They should not contain logic such as:
or:
as part of the conversation flow.
Storage-specific behavior should remain isolated behind the file storage implementation.
Attachment Domain Model
The application should also distinguish between the attachment metadata and the physical file storage.
Conceptually:
Attachment
├── id
├── conversation_id
├── message_id
├── file_name
├── content_type
├── size_bytes
├── storage_key
├── created_at
└── ...
storage_key should represent a generic reference understood by the storage layer.
The domain model should not expose S3-specific concepts such as:
bucket_name
s3_object_key
presigned_url
unless they exist strictly inside the S3 storage implementation.
For example, today:
storage_key = attachment-123
↓
DatabaseFileStorage
In the future:
storage_key = attachment-123
↓
S3FileStorage
↓
s3://some-bucket/attachments/attachment-123
The rest of Extra should not need to know which one is being used.
Persistence / Database
Attachment metadata should be persisted and associated with the corresponding conversation/message.
The database should persist enough information to:
- Associate the file with its message.
- Restore attachments when loading conversations.
- Know the original file name.
- Know the content type.
- Know the file size.
- Resolve the stored content through the generic storage layer.
If the initial implementation stores the binary itself in the DB, that should be treated as one implementation of the storage abstraction rather than as part of the core attachment model.
This is important so we can later move file content to S3 without changing the public attachment contract.
Message Model
A message should be able to reference zero or more attachments.
Conceptually:
{
"id": "message-123",
"role": "user",
"content": "Please analyze this file",
"attachments": [
{
"id": "attachment-456",
"fileName": "report.pdf",
"contentType": "application/pdf",
"sizeBytes": 2457600
}
]
}
Storage implementation details should not be exposed through this API.
Conversation History
When reopening an existing conversation:
Open Chat History
↓
Open conversation
↓
Load messages
↓
Load attachment metadata
↓
Resolve attachment through FileStorage
↓
Render attachments
Attachments must remain available after refresh and when reopening historical conversations.
Agent / LLM Flow
The uploaded file should also have a generic Extra-level representation when passed into the agent/model layer.
The file infrastructure must not be tightly coupled to:
- A specific LLM.
- A specific model provider.
- A specific storage provider.
Conceptually:
Stored Attachment
↓
Extra Attachment abstraction
↓
Model integration layer
↓
Provider-specific conversion
↓
Configured LLM
Provider-specific handling should happen only at the integration boundary.
Error Handling
Clear errors should exist for cases such as:
File too large
The selected file exceeds the maximum allowed size of 5 MB.
Upload failed
Failed to upload the file. Please try again.
Invalid attachment
The attached file could not be processed.
Storage failures should also be handled through the generic abstraction rather than leaking provider-specific errors to higher layers.
Failed uploads must not leave orphaned attachment records or inconsistent message references.
Expected Architecture
UI
│
▼
Upload / Message API
│
▼
Attachment Service
│
├── Validate metadata / 5 MB limit
│
├── Persist attachment metadata
│
▼
FileStorage abstraction
│
├── DatabaseFileStorage ← initial implementation if desired
├── S3FileStorage ← future
└── OtherStorage ← future
│
▼
Stored File
The key requirement is:
must not require changing:
UI
Conversation API
Message model
Conversation service
Agent execution flow
Only the storage implementation/configuration should need to change.
Acceptance Criteria
- Users can attach files from the chat UI.
- Maximum supported file size is 5 MB per file.
- UI validates the 5 MB limit.
- Backend independently validates the 5 MB limit.
- Relevant server/proxy limits support 5 MB uploads.
- Files larger than 5 MB return a clear error.
- Attachment metadata is persisted.
- Attachments are associated with the correct message and conversation.
- Attachments are restored when reopening conversations.
- Upload/loading/error states are handled in the UI.
- Failed uploads do not leave inconsistent or orphaned records.
- The implementation introduces a generic file storage abstraction.
- Business logic does not depend directly on database-based file storage.
- Storage-specific concepts do not leak into the public attachment/message model.
- The initial storage implementation can use the DB.
- A future S3 implementation can be introduced without changing the conversation/message APIs or business logic.
- The attachment model uses a generic storage reference/key.
- File handling remains independent of the configured LLM/provider.
- Tests cover size validation, persistence, retrieval, conversation reload, storage abstraction behavior, and failure scenarios.
Feature: Support File Uploads up to 5 MB
Goal
Add end-to-end support for uploading files to a conversation.
Users should be able to attach files from the UI, send them as part of the conversation flow, and have the backend persist and expose the required file information.
The maximum supported file size should be 5 MB per file.
This feature must be implemented across the full stack:
The limit must not exist only in the UI. It must be enforced consistently by the backend as well.
UI
The chat composer should allow the user to attach a file before sending a message.
The UI should support:
Client-side Validation
The UI should reject files larger than 5 MB before attempting the upload.
For example:
This is only a UX validation.
The backend must independently enforce the same limit.
Backend / API
The server should support receiving file attachments as part of the conversation/message flow.
Conceptually:
The backend must validate:
A request containing a file larger than 5 MB must be rejected by the server even if the client-side validation was bypassed.
For example:
413 Payload Too Largewith a clear response:
{ "error": "file_too_large", "message": "File size exceeds the maximum allowed size of 5 MB." }Server Configuration
The 5 MB limit should be supported at every server layer involved in handling the request.
This includes checking limits configured in:
We should avoid a situation where the UI allows 5 MB but another layer rejects the request at a smaller size.
The limit should preferably be defined from a single configuration value or constant.
Generic File Storage Architecture
The file implementation must be storage-agnostic.
We should not couple the upload feature directly to the current persistence mechanism.
For the initial implementation, the file content may be stored in the database if that fits the current architecture. However, the application layer should not depend on files being stored in the DB.
In the future, we may want to store file content in:
Changing the storage backend should not require changing the conversation, message, API, or UI layers.
The architecture should therefore introduce a generic file storage abstraction.
For example:
Conceptually:
Higher-level components should depend only on this abstraction:
They should not contain logic such as:
or:
as part of the conversation flow.
Storage-specific behavior should remain isolated behind the file storage implementation.
Attachment Domain Model
The application should also distinguish between the attachment metadata and the physical file storage.
Conceptually:
storage_keyshould represent a generic reference understood by the storage layer.The domain model should not expose S3-specific concepts such as:
unless they exist strictly inside the S3 storage implementation.
For example, today:
In the future:
The rest of Extra should not need to know which one is being used.
Persistence / Database
Attachment metadata should be persisted and associated with the corresponding conversation/message.
The database should persist enough information to:
If the initial implementation stores the binary itself in the DB, that should be treated as one implementation of the storage abstraction rather than as part of the core attachment model.
This is important so we can later move file content to S3 without changing the public attachment contract.
Message Model
A message should be able to reference zero or more attachments.
Conceptually:
{ "id": "message-123", "role": "user", "content": "Please analyze this file", "attachments": [ { "id": "attachment-456", "fileName": "report.pdf", "contentType": "application/pdf", "sizeBytes": 2457600 } ] }Storage implementation details should not be exposed through this API.
Conversation History
When reopening an existing conversation:
Attachments must remain available after refresh and when reopening historical conversations.
Agent / LLM Flow
The uploaded file should also have a generic Extra-level representation when passed into the agent/model layer.
The file infrastructure must not be tightly coupled to:
Conceptually:
Provider-specific handling should happen only at the integration boundary.
Error Handling
Clear errors should exist for cases such as:
File too large
Upload failed
Invalid attachment
Storage failures should also be handled through the generic abstraction rather than leaking provider-specific errors to higher layers.
Failed uploads must not leave orphaned attachment records or inconsistent message references.
Expected Architecture
The key requirement is:
must not require changing:
Only the storage implementation/configuration should need to change.
Acceptance Criteria