Skip to main content
  • 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 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 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.
FeatureFunction basedEndpoint based
Best ForComplex setups requiring middleware logic, dynamic headers, or transformation before sendingStandard REST APIs where you just need to pass the request “as-is” to the backend
ImplementationYou write the fetch() or axios codeYou provide the url string and headers object
FlexibilityHighMedium
SpeedMediumHigh

Endpoint based DataProvider

Instead of implementing custom methods, you can configure endpoints directly and let the SDK handle HTTP requests.
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.
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>
// 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.
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>
// 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

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 }
};

const attachmentDataProvider = {
  config: attachmentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ attachment: attachmentDataProvider }}
>
</VeltProvider>

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.
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>

delete

Delete attachments from your storage backend. Return a success or error response. On error we will retry.
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>

config

Configuration for the attachment data provider.
  • Type: ResolverConfig. Relevant properties:
    • resolveTimeout: Timeout duration (in milliseconds) for resolver operations
    • saveRetryConfig: RetryConfig. Configure retry behavior for save operations.
    • deleteRetryConfig: RetryConfig. Configure retry behavior for delete operations.
const attachmentResolverConfig = {
  resolveTimeout: 5000,
  saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
  deleteRetryConfig: { retryCount: 3, retryDelay: 2000 }
};

Function based Complete Example

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 }
};

const attachmentDataProvider = {
  save: saveAttachmentsToDB,
  delete: deleteAttachmentsFromDB,
  config: attachmentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ attachment: attachmentDataProvider }}
>
</VeltProvider>

Sample Data

{
    "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.

Debugging

You can subscribe to dataProvider events to monitor and debug save and delete operations. You can also use the Velt Chrome DevTools extension to inspect and debug your Velt implementation.
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]);