Add production-shaped attachments to the corporate AI chat. Users can attach files to a message, see them in history, and the configured AI Gateway receives temporary, authenticated access to them.
Create a chat_attachments table:
message_id foreign key with cascading delete;user_id foreign key, used for authorization checks;Objects are stored under a non-guessable key users/<user-id>/attachments/<attachment-id>. The S3 bucket is private. Database rows are created only after a successful object write; an unsuccessful database write schedules/deletes the newly written object. Deleting a conversation cascades attachment metadata; application cleanup deletes corresponding objects.
POST /api/attachments accepts multipart FormData containing one file. It requires the existing Bearer JWT and returns attachment metadata for a temporary, not-yet-sent upload. The upload belongs to the authenticated user and expires after 24 hours if it is not linked to a message.
POST /api/chat accepts JSON with the existing fields plus attachmentIds: string[]. It verifies every attachment belongs to the current user and is pending, creates the user message, atomically attaches the records to it, then starts the assistant stream.
GET /api/attachments/:id verifies owner access and returns a short-lived redirect or stream from the private S3 object. User JWTs never reach object storage or the AI Gateway.
The outbound AI Gateway request gains an attachments array alongside model and messages:
{
"id": "attachment UUID",
"name": "report.pdf",
"contentType": "application/pdf",
"size": 182736,
"url": "short-lived signed URL"
}
The present mock gateway reports which attachment names it received. A real AI Gateway decides which MIME types it can inspect and fetches the temporary URLs itself.
Request.formData() has no App Router size limit, so application-level checks are mandatory; the deployment proxy must enforce the same 20 MiB request limit.The attach control is keyboard accessible and opens the native file picker. Dragging valid files over the composer shows a drop target. Selected files upload immediately, show per-file progress/error state and can be removed before the message is sent. The send button remains disabled while an upload is pending or failed. A text-only message continues to work unchanged.