> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Attachments

> Self-host your comments file attachments data while using Velt's components. Keep attachment storage on your infrastructure with minimal metadata stored on Velt servers.

> [!WARNING]
>
> * This is currently only compatible with `setDocuments` method.
> * Ensure that the data providers are set prior to calling `identify` method.
> * The data provider methods must return the correct status code (e.g. 200 for success, 500 for errors) and success boolean in the response object. This ensures proper error handling and retries.

# Overview

Velt supports self-hosting your comments file attachments data:

* Attachments can be stored on your own infrastructure, with only necessary identifiers on Velt servers.
* Velt Components automatically hydrate attachment data in the frontend by fetching from your configured data provider.
* This gives you full control over attachment data while maintaining the file attachment features.

# How does it work?

When users upload or delete attachments:

1. The SDK uses your configured [`AttachmentDataProvider`](https://velt.dev/docs/api-reference/sdk/models/data-models#attachmentdataprovider) to handle storage
2. Your data provider implements two key methods:
   * `save`: Stores the file and returns its URL
   * `delete`: Removes the file from storage

**The process works as follows:**

When an attachment operation occurs:

1. The SDK first attempts to save/delete the file on your storage infrastructure
2. If successful:
   * The SDK updates Velt's servers with minimal metadata
   * The [`PartialComment`](https://velt.dev/docs/api-reference/sdk/models/data-models#partialcomment) object is updated to reference the attachment including the attachment url, name and metadata.
   * When the comment is saved, this information is stored on your end.
   * Velt servers only store necessary identifiers, not the actual files or URLs
3. If the operation fails, no changes are made to Velt's servers and the operation is retried if you have configured retries.

# Implementation Approaches

You can implement attachment self-hosting using either of these approaches:

1. **Endpoint based**: Provide endpoint URLs and let the SDK handle HTTP requests
2. **Function based**: Implement `save` and `delete` methods yourself

Both approaches are fully backward compatible and can be used together.

| Feature | Function based | Endpoint based |
| - | - | - |
| **Best For** | Complex setups requiring middleware logic, dynamic headers, or transformation before sending | Standard REST APIs where you just need to pass the request "as-is" to the backend |
| **Implementation** | You write the `fetch()` or `axios` code | You provide the endpoint `url`, static or async `headers`, and optional `credentials` |
| **Flexibility** | High | Medium |
| **Speed** | Medium | High |

## Endpoint based DataProvider

Instead of implementing custom methods, you can configure endpoints directly and let the SDK handle HTTP requests.

> [!NOTE]
>
> `headers` may also be an async function resolved per request, and `credentials` (`'include' | 'same-origin' | 'omit'`) enables cookie/session auth — see [Async headers and credentials](https://velt.dev/docs/self-hosting/partial/overview.md#async-headers-and-credentials) for details.

> [!NOTE]
>
> Unlike comments and reactions, attachment upload uses `multipart/form-data` format to avoid base64 conversion overhead. The browser automatically manages the Content-Type header with the proper boundary parameter.

### saveConfig

Config-based endpoint for saving attachments. The SDK automatically makes HTTP POST requests with multipart/form-data.

* Type: [`ResolverEndpointConfig`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverendpointconfig)
* Request format: `multipart/form-data` with two fields:
  * `file`: File object (binary)
  * `request`: JSON string containing [`SaveAttachmentResolverRequest`](https://velt.dev/docs/api-reference/sdk/models/data-models#saveattachmentresolverrequest)
* Response format: [`ResolverResponse<SaveAttachmentResolverData>`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverresponse)

> [tab] React / Next.js
>
> ```jsx theme={null}
> const attachmentResolverConfig = {
>   saveConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/save',
>     headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
>     // Note: Content-Type is automatically set by browser
>   }
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```js theme={null}
> const attachmentResolverConfig = {
>   saveConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/save',
>     headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
>     // Note: Content-Type is automatically set by browser
>   }
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

> [tab] Backend Endpoint Example (S3)
>
> ```javascript theme={null}
> // Backend handler for /api/velt/attachments/save
> const file = req.file;
> const request = JSON.parse(req.body.request);
>
> // Use metadata to organize files
> const { metadata } = request;
> const { organizationId, documentId } = metadata;
> const key = `attachments/${organizationId}/${documentId}/${Date.now()}-${file.originalname}`;
>
> await s3Client.send(new PutObjectCommand({
>   Bucket: process.env.S3_BUCKET,
>   Key: key,
>   Body: file.buffer,
>   ContentType: file.mimetype
> }));
>
> const url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`;
>
> // Return response in required format
> res.json({ data: { url }, success: true, statusCode: 200 });
> ```

### deleteConfig

Config-based endpoint for deleting attachments. The SDK automatically makes HTTP POST requests with the request body.

* Type: [`ResolverEndpointConfig`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverendpointconfig)
* Request body format: [`DeleteAttachmentResolverRequest`](https://velt.dev/docs/api-reference/sdk/models/data-models#deleteattachmentresolverrequest)
* Response format: [`ResolverResponse<undefined>`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverresponse)

> [tab] React / Next.js
>
> ```jsx theme={null}
> const attachmentResolverConfig = {
>   deleteConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/delete',
>     headers: {
>       'Content-Type': 'application/json',
>       'Authorization': 'Bearer YOUR_TOKEN'
>     }
>   }
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```js theme={null}
> const attachmentResolverConfig = {
>   deleteConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/delete',
>     headers: {
>       'Content-Type': 'application/json',
>       'Authorization': 'Bearer YOUR_TOKEN'
>     }
>   }
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

> [tab] Backend Endpoint Example (S3)
>
> ```javascript theme={null}
> // Backend handler for /api/velt/attachments/delete
> const { url } = req.body;
>
> // Delete file from S3
> const key = url.split('.s3.amazonaws.com/')[1];
>
> await s3Client.send(new DeleteObjectCommand({
>   Bucket: process.env.S3_BUCKET,
>   Key: key
> }));
>
> // Return response in required format
> res.json({ success: true, statusCode: 200 });
> ```

### Endpoint based Complete Example

> [tab] React / Next.js
>
> ```jsx theme={null}
> const attachmentResolverConfig = {
>   saveConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/save',
>     headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
>   },
>   deleteConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/delete',
>     headers: {
>       'Content-Type': 'application/json',
>       'Authorization': 'Bearer YOUR_TOKEN'
>     }
>   },
>   resolveTimeout: 5000,
>   saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   additionalFields: ['mimeType', 'size']
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```js theme={null}
> const attachmentResolverConfig = {
>   saveConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/save',
>     headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
>   },
>   deleteConfig: {
>     url: 'https://your-backend.com/api/velt/attachments/delete',
>     headers: {
>       'Content-Type': 'application/json',
>       'Authorization': 'Bearer YOUR_TOKEN'
>     }
>   },
>   resolveTimeout: 5000,
>   saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   additionalFields: ['mimeType', 'size']
> };
>
> const attachmentDataProvider = {
>   config: attachmentResolverConfig
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

## Function based DataProvider

Implement custom methods to handle data operations yourself.

### save

Save attachments to your storage backend. Return the url with a success or error response. On error we will retry.

* Param: [`SaveAttachmentResolverRequest`](https://velt.dev/docs/api-reference/sdk/models/data-models#saveattachmentresolverrequest)
* Return: [`Promise<ResolverResponse<SaveAttachmentResolverData>>`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverresponse)

> [tab] Frontend Example
>
> ```jsx React / Next.js theme={null}
> const saveAttachmentsToDB = async (request) => {
>   const formData = new FormData();
>   formData.append('file', request.file);
>   formData.append('metadata', JSON.stringify(request.metadata));
>
>   const response = await fetch('/api/velt/attachments/save', {
>     method: 'POST',
>     body: formData
>   });
>   return await response.json();
> };
>
> const attachmentDataProvider = {
>   save: saveAttachmentsToDB,
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```
>
> ```js Other Frameworks theme={null}
> const saveAttachmentsToDB = async (request) => {
>   const formData = new FormData();
>   formData.append('file', request.file);
>   formData.append('metadata', JSON.stringify(request.metadata));
>
>   const response = await fetch('/api/velt/attachments/save', {
>     method: 'POST',
>     body: formData
>   });
>   return await response.json();
> };
>
> const attachmentDataProvider = {
>   save: saveAttachmentsToDB,
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

> [tab] Backend Endpoint Example (S3)
>
> ```javascript theme={null}
> const file = req.file;
> const metadata = JSON.parse(req.body.metadata);
>
> // Use metadata to organize files by organization and document
> const { organizationId, documentId } = metadata;
> const key = `attachments/${organizationId}/${documentId}/${Date.now()}-${file.originalname}`;
>
> await s3Client.send(new PutObjectCommand({
>   Bucket: process.env.S3_BUCKET,
>   Key: key,
>   Body: file.buffer,
>   ContentType: file.mimetype
> }));
>
> const url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`;
>
> // Return response in required format
> res.json({ data: { url }, success: true, statusCode: 200 });
> ```

### delete

Delete attachments from your storage backend. Return a success or error response. On error we will retry.

* Param: [`DeleteAttachmentResolverRequest`](https://velt.dev/docs/api-reference/sdk/models/data-models#deleteattachmentresolverrequest)
* Return: [`Promise<ResolverResponse<undefined>>`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverresponse)

> [tab] Frontend Example
>
> ```jsx React / Next.js theme={null}
> const deleteAttachmentsFromDB = async (request) => {
>   const response = await fetch('/api/velt/attachments/delete', {
>     method: 'POST',
>     headers: { 'Content-Type': 'application/json' },
>     body: JSON.stringify(request)
>   });
>   return await response.json();
> };
>
> const attachmentDataProvider = {
>   delete: deleteAttachmentsFromDB,
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```
>
> ```js Other Frameworks theme={null}
> const deleteAttachmentsFromDB = async (request) => {
>   const response = await fetch('/api/velt/attachments/delete', {
>     method: 'POST',
>     headers: { 'Content-Type': 'application/json' },
>     body: JSON.stringify(request)
>   });
>   return await response.json();
> };
>
> const attachmentDataProvider = {
>   delete: deleteAttachmentsFromDB,
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

> [tab] Backend Endpoint Example (S3)
>
> ```javascript theme={null}
> const { url } = req.body;
>
> // Delete file from S3
> const key = url.split('.s3.amazonaws.com/')[1];
>
> await s3Client.send(new DeleteObjectCommand({
>   Bucket: process.env.S3_BUCKET,
>   Key: key
> }));
>
> // Return response in required format
> res.json({ success: true, statusCode: 200 });
> ```

### config

Configuration for the attachment data provider.

* Type: [`ResolverConfig`](https://velt.dev/docs/api-reference/sdk/models/data-models#resolverconfig). Relevant properties:
  * `resolveTimeout`: Timeout duration (in milliseconds) for resolver operations
  * `saveRetryConfig`: [`RetryConfig`](https://velt.dev/docs/api-reference/sdk/models/data-models#retryconfig). Configure retry behavior for save operations.
  * `deleteRetryConfig`: [`RetryConfig`](https://velt.dev/docs/api-reference/sdk/models/data-models#retryconfig). Configure retry behavior for delete operations.
  * `additionalFields`: `string[]`. Specify additional fields from the attachment object to include in the resolver request payloads sent to your backend. By default, only core content fields are sent. Use this to request extra fields you need (e.g., `mimeType`, `size`).

```jsx theme={null}
const attachmentResolverConfig = {
  resolveTimeout: 5000,
  saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
  deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
  additionalFields: ['mimeType', 'size']
};
```

### Function based Complete Example

> [tab] React / Next.js
>
> ```jsx theme={null}
> const saveAttachmentsToDB = async (request) => {
>   const formData = new FormData();
>   formData.append('file', request.file);
>   formData.append('metadata', JSON.stringify(request.metadata));
>
>   const response = await fetch('/api/velt/attachments/save', {
>     method: 'POST',
>     body: formData
>   });
>   return await response.json();
> };
>
> const deleteAttachmentsFromDB = async (request) => {
>   const response = await fetch('/api/velt/attachments/delete', {
>     method: 'POST',
>     headers: { 'Content-Type': 'application/json' },
>     body: JSON.stringify(request)
>   });
>   return await response.json();
> };
>
> const attachmentResolverConfig = {
>   resolveTimeout: 5000,
>   saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   additionalFields: ['mimeType', 'size']
> };
>
> const attachmentDataProvider = {
>   save: saveAttachmentsToDB,
>   delete: deleteAttachmentsFromDB,
>   config: attachmentResolverConfig
> };
>
> <VeltProvider
>   apiKey='YOUR_API_KEY'
>   dataProviders={{ attachment: attachmentDataProvider }}
> >
> </VeltProvider>
> ```

> [tab] Other Frameworks
>
> ```js theme={null}
> const saveAttachmentsToDB = async (request) => {
>   const formData = new FormData();
>   formData.append('file', request.file);
>   formData.append('metadata', JSON.stringify(request.metadata));
>
>   const response = await fetch('/api/velt/attachments/save', {
>     method: 'POST',
>     body: formData
>   });
>   return await response.json();
> };
>
> const deleteAttachmentsFromDB = async (request) => {
>   const response = await fetch('/api/velt/attachments/delete', {
>     method: 'POST',
>     headers: { 'Content-Type': 'application/json' },
>     body: JSON.stringify(request)
>   });
>   return await response.json();
> };
>
> const attachmentResolverConfig = {
>   resolveTimeout: 5000,
>   saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
>   additionalFields: ['mimeType', 'size']
> };
>
> const attachmentDataProvider = {
>   save: saveAttachmentsToDB,
>   delete: deleteAttachmentsFromDB,
>   config: attachmentResolverConfig
> };
>
> Velt.setDataProviders({ attachment: attachmentDataProvider });
> ```

# Sample Data

> [tab] Comment annotation stored on your database
>
> ```json theme={null}
> {
>     "annotationId": "ANNOTATION_ID",
>     "metadata": {
>         "apiKey": "API_KEY",
>         "documentId": "DOCUMENT_ID",
>         "organizationId": "ORGANIZATION_ID"
>     },
>     "comments": {
>         "184639": {
>             "commentId": 184639,
>             "commentHtml": "<p>Hello</p>",
>             "commentText": "Hello",
>             "from": {
>                 "userId": "USER_ID"
>             }
>         },
>         "743772": {
>             "commentId": 743772,
>             "attachments": {
>                 "758336": {
>                     "url": "https://your-bucket.s3.amazonaws.com/attachments/API_KEY/ATTACHMENT_ID.png",
>                     "name": "image.png",
>                     "attachmentId": 758336
>                 }
>             },
>             "from": {
>                 "userId": "USER_ID"
>             }
>         }
>     }
> }
> ```
>
> When an attachment is added, the comment object is updated with the attachment details including the URL and name from your storage.

> [tab] Attachment object stored on your database
>
> ```json theme={null}
> {
>     "attachmentId": 758336,
>     "metadata": {
>         "organizationId": "ORGANIZATION_ID",
>         "documentId": "DOCUMENT_ID",
>         "apiKey": "API_KEY"
>     },
>     "url": "https://your-bucket.s3.amazonaws.com/attachments/API_KEY/ATTACHMENT_ID.png",
>     "mimeType": "image/png",
>     "name": "image.png"
> }
> ```
>
> This is a separate attachments collection on your database that stores the full attachment details.

> [tab] Stored on Velt servers
>
> ```json theme={null}
> {
>     "annotationId": "ANNOTATION_ID",
>     "comments": [
>         {
>             "commentId": 184639,
>             "from": {
>                 "userId": "USER_ID"
>             },
>             "isCommentResolverUsed": true,
>             "isCommentTextAvailable": true,
>             "attachments": []
>         },
>         {
>             "commentId": 743772,
>             "from": {
>                 "userId": "USER_ID"
>             },
>             "isCommentResolverUsed": true,
>             "attachments": [
>                 {
>                     "attachmentId": 758336,
>                     "isAttachmentResolverUsed": true,
>                     "mimeType": "image/png",
>                     "size": 289870,
>                     "type": "png"
>                 }
>             ]
>         }
>     ],
>     "metadata": {
>         "apiKey": "API_KEY",
>         "documentId": "DOCUMENT_ID",
>         "organizationId": "ORGANIZATION_ID"
>     }
> }
> ```
>
> Only attachment identifiers, bucket path, and file metadata (mimeType, size, type) are stored on Velt servers. The actual file URL and name remain on your storage infrastructure.

# Debugging

You can subscribe to `dataProvider` events to monitor and debug save and delete operations.

You can also use the [Velt Chrome DevTools extension](https://chromewebstore.google.com/detail/velt-devtools/nfldoicbagllmegffdapcnohakpamlnl) to inspect and debug your Velt implementation.

> [tab] React / Next.js
>
> ```jsx theme={null}
> import { useVeltClient } from '@veltdev/react';
>
> const { client } = useVeltClient();
>
> useEffect(() => {
>   if (!client) return;
>
>   const subscription = client.on('dataProvider').subscribe((event) => {
>     console.log('Data Provider Event:', event);
>   });
>
>   return () => subscription?.unsubscribe();
> }, [client]);
> ```

> [tab] Other Frameworks
>
> ```javascript theme={null}
> const subscription = Velt.on('dataProvider').subscribe((event) => {
>   console.log('Data Provider Event:', event);
> });
>
> // Unsubscribe when done
> subscription?.unsubscribe();
> ```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.