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:
The SDK uses your configured AttachmentDataProvider to handle storage
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:
The SDK first attempts to save/delete the file on your storage infrastructure
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
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:
Endpoint based : Provide endpoint URLs and let the SDK handle HTTP requests
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 url string and headers object 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.
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.
React / Next.js
Other Frameworks
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 >
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 });
// 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.
React / Next.js
Other Frameworks
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 >
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 });
// 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
React / Next.js
Other Frameworks
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 >
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
};
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.
React / Next.js
Other Frameworks
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 >
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.
React / Next.js
Other Frameworks
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 >
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 . 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
React / Next.js
Other Frameworks
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 >
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
};
Velt . setDataProviders ({ attachment: attachmentDataProvider });
Sample Data
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.
React / Next.js
Other Frameworks
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 ]);
const subscription = Velt . on ( 'dataProvider' ). subscribe (( event ) => {
console . log ( 'Data Provider Event:' , event );
});
// Unsubscribe when done
subscription ?. unsubscribe ();