|
| 1 | +--- |
| 2 | +ADR: 0030 |
| 3 | +Title: Chat attachments via Vercel Blob client uploads + /api/upload/register (hosted URLs + inline AI Elements) |
| 4 | +Status: Implemented |
| 5 | +Version: 0.1 |
| 6 | +Date: 2026-02-10 |
| 7 | +Supersedes: [] |
| 8 | +Superseded-by: [] |
| 9 | +Related: [ADR-0011, ADR-0009, ADR-0006, ADR-0026] |
| 10 | +Tags: [chat, attachments, ai-elements, ai-sdk, upload, ingestion, workflow] |
| 11 | +Related-Requirements: [FR-003, FR-008, FR-019, PR-001, NFR-008, NFR-010, IR-006] |
| 12 | +References: |
| 13 | + - [AI Elements attachments](https://elements.ai-sdk.dev/components/attachments.md) |
| 14 | + - [AI SDK useChat attachments](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#attachments) |
| 15 | + - [AI SDK FileUIPart](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message#fileuipart) |
| 16 | +--- |
| 17 | + |
| 18 | +## Status |
| 19 | + |
| 20 | +Implemented — 2026-02-10. |
| 21 | + |
| 22 | +## Context |
| 23 | + |
| 24 | +Project Chat is the primary UX for interacting with a project knowledge base. |
| 25 | +We already support: |
| 26 | + |
| 27 | +- uploading documents to a project (stored in Blob, ingested, indexed) |
| 28 | +- multi-turn durable chat sessions (Workflow DevKit) |
| 29 | +- AI Elements primitives for chat UI |
| 30 | + |
| 31 | +We need first-class **document attachments** in chat so users can: |
| 32 | + |
| 33 | +1. attach a file in the chat composer |
| 34 | +2. have it ingested for RAG grounding |
| 35 | +3. see the attachment in message history |
| 36 | +4. attach files in follow-up messages during an active durable chat run |
| 37 | + |
| 38 | +## Decision Drivers |
| 39 | + |
| 40 | +- Reuse the existing, validated upload + ingestion pipeline: |
| 41 | + - `POST /api/upload` for Vercel Blob client upload token exchange |
| 42 | + - `POST /api/upload/register` for registration, dedupe, and ingestion |
| 43 | +- Avoid base64/data URL message payloads for performance and reliability. |
| 44 | +- Preserve resume-safe chat history (stream markers + persisted UI messages). |
| 45 | +- Keep model prompts clean; rely on ingestion + retrieval for documents. |
| 46 | +- Minimize new endpoints and contracts; strict TypeScript + Zod validation. |
| 47 | + |
| 48 | +## Alternatives Considered |
| 49 | + |
| 50 | +### A. Embed attachments directly as data URLs in chat requests |
| 51 | + |
| 52 | +Pros: |
| 53 | + |
| 54 | +- Single request; no separate upload call. |
| 55 | + |
| 56 | +Cons: |
| 57 | + |
| 58 | +- Large payloads, memory pressure, and brittle streaming on non-trivial docs. |
| 59 | +- Harder to persist and replay reliably. |
| 60 | + |
| 61 | +### B. Create a dedicated chat upload endpoint (`/api/chat/upload`) |
| 62 | + |
| 63 | +Pros: |
| 64 | + |
| 65 | +- Chat-specific contract could include `messageId`, status, etc. |
| 66 | + |
| 67 | +Cons: |
| 68 | + |
| 69 | +- Duplicates the existing allowlist/ingest/dedupe pipeline. |
| 70 | +- Adds long-term maintenance surface area for minimal value. |
| 71 | + |
| 72 | +### C. Use Vercel Blob client uploads + `POST /api/upload/register` and send hosted file URLs as `FileUIPart` (**Chosen**) |
| 73 | + |
| 74 | +Pros: |
| 75 | + |
| 76 | +- One canonical ingestion path for the whole app. |
| 77 | +- Hosted URLs keep chat payloads small; `FileUIPart.url` supports hosted URLs.[^ai-sdk-fileuipart] |
| 78 | +- Clean separation: upload/ingest first, then chat. |
| 79 | +- Works for follow-ups by extending `POST /api/chat/:runId` contract. |
| 80 | + |
| 81 | +Cons: |
| 82 | + |
| 83 | +- Sync ingest can add latency; may need a hybrid/async ingest UX later. |
| 84 | +- Blob URLs are public; requires care around lifecycle/cleanup (future). |
| 85 | + |
| 86 | +### D. Stream multipart upload through the chat streaming endpoint |
| 87 | + |
| 88 | +Pros: |
| 89 | + |
| 90 | +- Single action; can stream progress inline. |
| 91 | + |
| 92 | +Cons: |
| 93 | + |
| 94 | +- High complexity (multipart streaming + retries + partial failure handling). |
| 95 | +- Not needed for docs-only scope. |
| 96 | + |
| 97 | +## Decision Framework (must be ≥ 9.0) |
| 98 | + |
| 99 | +Weights: |
| 100 | + |
| 101 | +- Solution leverage: 35% |
| 102 | +- Application value: 30% |
| 103 | +- Maintenance and cognitive load: 25% |
| 104 | +- Architectural adaptability: 10% |
| 105 | + |
| 106 | +| Option | Leverage | Value | Maintenance | Adaptability | Weighted total | |
| 107 | +| --- | ---: | ---: | ---: | ---: | ---: | |
| 108 | +| A. Data URLs in chat | 6.8 | 7.4 | 5.8 | 7.2 | 6.79 | |
| 109 | +| B. Dedicated chat upload endpoint | 7.2 | 8.1 | 7.0 | 8.0 | 7.46 | |
| 110 | +| C. Blob client uploads + `/api/upload/register` + hosted URLs | 9.4 | 9.2 | 8.9 | 9.0 | **9.19** | |
| 111 | +| D. Multipart streaming upload | 6.4 | 8.0 | 4.2 | 8.6 | 6.63 | |
| 112 | + |
| 113 | +## Decision |
| 114 | + |
| 115 | +Implement chat attachments as: |
| 116 | + |
| 117 | +1. Composer uses vendored AI Elements `PromptInput` attachments + `Attachments` inline UI.[^ai-elements-attachments] |
| 118 | +2. Client uploads attachment bytes via `@vercel/blob/client upload()` using `POST /api/upload` as the token exchange endpoint. |
| 119 | +3. Client registers and ingests the uploaded blobs via `POST /api/upload/register` (sync ingest by default). |
| 120 | +4. Client sends the chat message using hosted file URLs (no data URLs) as `FileUIPart[]` alongside text. |
| 121 | +5. Extend durable follow-ups: `POST /api/chat/:runId` accepts `files?: FileUIPart[]` and resumes the workflow hook with attachments. |
| 122 | +6. Persist file parts in UI message history and include them in `data-workflow` user-message markers for resume-safe replay. |
| 123 | +7. Do not pass document file parts to the model directly; strip file parts when building model messages and append a filename note, relying on ingestion + retrieval (SPEC-0004). |
| 124 | + |
| 125 | +Implementation details and contracts are specified in: |
| 126 | + |
| 127 | +- [SPEC-0029](../spec/SPEC-0029-chat-attachments.md) |
| 128 | + |
| 129 | +## Consequences |
| 130 | + |
| 131 | +### Positive outcomes |
| 132 | + |
| 133 | +- Users can attach documents in chat and see them in history. |
| 134 | +- Attachments in follow-ups work during active durable sessions. |
| 135 | +- Upload/ingestion remains canonical and deduped. |
| 136 | +- Chat payloads remain small and stream-resume-safe. |
| 137 | + |
| 138 | +### Negative outcomes / risks |
| 139 | + |
| 140 | +- Sync ingest can add noticeable latency for large docs. |
| 141 | + - Mitigation: keep strict upload budgets; consider a hybrid or async ingest UX if needed. |
| 142 | +- Public blob URLs can be shared out-of-band. |
| 143 | + - Mitigation: keep app access private; consider signed URLs or authenticated proxying in a future hardening pass. |
| 144 | +- Orphaned uploads if the user abandons mid-flow. |
| 145 | + - Mitigation (future): add cleanup/GC for unattached uploads or reference counting by project. |
| 146 | + |
| 147 | +## References |
| 148 | + |
| 149 | +[^ai-elements-attachments]: <https://elements.ai-sdk.dev/components/attachments.md> |
| 150 | +[^ai-sdk-fileuipart]: <https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message#fileuipart> |
0 commit comments