Documentation Index Fetch the complete documentation index at: https://docs.velt.dev/llms.txt
Use this file to discover all available pages before exploring further.
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 reactions and related data:
Reactions can be stored on your own infrastructure, with only necessary identifiers on Velt servers.
Velt Components automatically hydrate reaction data in the frontend by fetching from your configured data provider.
This gives you full control over reaction data while maintaining all Velt collaboration features.
This automatically also ensures that the in-app notifications content related to reactions is not stored on Velt servers. The content is generated using the reactions data in the frontend.
How does it work?
When users add or remove reactions:
The SDK uses your configured ReactionAnnotationDataProvider to handle storage
Your data provider implements three key methods:
get: Fetches reactions from your database
save: Stores reactions and returns success/error
delete: Removes reactions from your database
The process works as follows:
When a reaction operation occurs:
The SDK first attempts to save/delete the reaction on your database
If successful:
The SDK updates Velt’s servers with minimal metadata
The PartialReactionAnnotation object is updated with the reaction details including emoji, user, and metadata
When the reaction is saved, this information is stored on your end
Velt servers only store necessary identifiers, not the actual reaction content
If the operation fails, no changes are made to Velt’s servers and the operation is retried if you have configured retries.
You can configure retries, timeouts, etc. for the data provider.
Implementation Approaches
You can implement reaction self-hosting using either of these approaches:
Endpoint based : Provide endpoint URLs and let the SDK handle HTTP requests
Function based : Implement get, 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.
getConfig
Config-based endpoint for fetching reactions. The SDK automatically makes HTTP POST requests with the request body.
React / Next.js
Other Frameworks
const reactionResolverConfig = {
getConfig: {
url: 'https://your-backend.com/api/velt/reactions/get' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const reactionResolverConfig = {
getConfig: {
url: 'https://your-backend.com/api/velt/reactions/get' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
Velt . setDataProviders ({ reaction: reactionDataProvider });
saveConfig
Config-based endpoint for saving reactions. The SDK automatically makes HTTP POST requests with the request body.
React / Next.js
Other Frameworks
const reactionResolverConfig = {
saveConfig: {
url: 'https://your-backend.com/api/velt/reactions/save' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const reactionResolverConfig = {
saveConfig: {
url: 'https://your-backend.com/api/velt/reactions/save' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
Velt . setDataProviders ({ reaction: reactionDataProvider });
deleteConfig
Config-based endpoint for deleting reactions. The SDK automatically makes HTTP POST requests with the request body.
React / Next.js
Other Frameworks
const reactionResolverConfig = {
deleteConfig: {
url: 'https://your-backend.com/api/velt/reactions/delete' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const reactionResolverConfig = {
deleteConfig: {
url: 'https://your-backend.com/api/velt/reactions/delete' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
}
};
const reactionDataProvider = {
config: reactionResolverConfig
};
Velt . setDataProviders ({ reaction: reactionDataProvider });
Endpoint based Complete Example
React / Next.js
Other Frameworks
const reactionResolverConfig = {
getConfig: {
url: 'https://your-backend.com/api/velt/reactions/get' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
saveConfig: {
url: 'https://your-backend.com/api/velt/reactions/save' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
deleteConfig: {
url: 'https://your-backend.com/api/velt/reactions/delete' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
resolveTimeout: 2000 ,
getRetryConfig: { retryCount: 3 , retryDelay: 2000 },
saveRetryConfig: { retryCount: 3 , retryDelay: 2000 },
deleteRetryConfig: { retryCount: 3 , retryDelay: 2000 },
additionalFields: [ 'commentAnnotationId' , 'type' ]
};
const reactionDataProvider = {
config: reactionResolverConfig
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const reactionResolverConfig = {
getConfig: {
url: 'https://your-backend.com/api/velt/reactions/get' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
saveConfig: {
url: 'https://your-backend.com/api/velt/reactions/save' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
deleteConfig: {
url: 'https://your-backend.com/api/velt/reactions/delete' ,
headers: { 'Authorization' : 'Bearer YOUR_TOKEN' }
},
resolveTimeout: 2000 ,
getRetryConfig: { retryCount: 3 , retryDelay: 2000 },
saveRetryConfig: { retryCount: 3 , retryDelay: 2000 },
deleteRetryConfig: { retryCount: 3 , retryDelay: 2000 },
additionalFields: [ 'commentAnnotationId' , 'type' ]
};
const reactionDataProvider = {
config: reactionResolverConfig
};
Velt . setDataProviders ({ reaction: reactionDataProvider });
Function based DataProvider
Implement custom methods to handle data operations yourself.
get
Method to fetch reactions from your database. On error we will retry.
React / Next.js
Other Frameworks
const fetchReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/get' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const reactionDataProvider = {
get: fetchReactionsFromDB ,
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
// Build query from request
const { reactionAnnotationIds , documentIds , organizationId } = req . body ;
const query = {};
if ( reactionAnnotationIds ?. length ) {
query . annotationId = { $in: reactionAnnotationIds };
}
if ( documentIds ?. length ) {
query . documentId = { $in: documentIds };
}
if ( organizationId ) {
query . organizationId = organizationId ;
}
const annotations = await collection . find ( query ). toArray ();
// Convert to Record<annotationId, annotation>
const result = {};
for ( const annotation of annotations ) {
result [ annotation . annotationId ] = annotation ;
}
// Return response in required format
res . json ({ data: result , success: true , statusCode: 200 });
// Build parameterized query
const { reactionAnnotationIds , documentIds , organizationId } = req . body ;
const conditions = [];
const values = [];
let paramIndex = 1 ;
if ( reactionAnnotationIds ?. length ) {
conditions . push ( `annotation_id = ANY($ ${ paramIndex ++ } )` );
values . push ( reactionAnnotationIds );
}
if ( documentIds ?. length ) {
conditions . push ( `document_id = ANY($ ${ paramIndex ++ } )` );
values . push ( documentIds );
}
if ( organizationId ) {
conditions . push ( `organization_id = $ ${ paramIndex ++ } ` );
values . push ( organizationId );
}
const whereClause = conditions . length ? `WHERE ${ conditions . join ( ' AND ' ) } ` : '' ;
const { rows } = await client . query (
`SELECT annotation_id, data FROM reaction_annotations ${ whereClause } ` ,
values
);
// Convert to Record<annotationId, annotation>
const result = {};
for ( const row of rows ) {
result [ row . annotation_id ] = row . data ;
}
// Return response in required format
res . json ({ data: result , success: true , statusCode: 200 });
save
Save reactions to your database. Return a success or error response. On error we will retry.
React / Next.js
Other Frameworks
const saveReactionsToDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/save' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const reactionDataProvider = {
save: saveReactionsToDB ,
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const { annotations , context } = req . body ;
// Bulk upsert annotations
const operations = Object . entries ( annotations ). map (([ id , annotation ]) => ({
updateOne: {
filter: { annotationId: id },
update: {
$set: {
... annotation ,
annotationId: id ,
documentId: context ?. documentId || annotation . documentId ,
organizationId: context ?. organizationId || annotation . organizationId ,
}
},
upsert: true
}
}));
if ( operations . length > 0 ) {
await collection . bulkWrite ( operations );
}
// Return response in required format
res . json ({ success: true , statusCode: 200 });
const { annotations , context } = req . body ;
// Transaction-based upsert
await client . query ( 'BEGIN' );
for ( const [ id , annotation ] of Object . entries ( annotations )) {
const data = { ... annotation , annotationId: id };
await client . query (
`INSERT INTO reaction_annotations (annotation_id, document_id, organization_id, data, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (annotation_id)
DO UPDATE SET data = EXCLUDED.data, updated_at = NOW()` ,
[ id , annotation . documentId , annotation . organizationId , JSON . stringify ( data )]
);
}
await client . query ( 'COMMIT' );
// Return response in required format
res . json ({ success: true , statusCode: 200 });
delete
Delete reactions from your database. Return a success or error response. On error we will retry.
React / Next.js
Other Frameworks
const deleteReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/delete' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const reactionDataProvider = {
delete: deleteReactionsFromDB ,
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const { annotationId } = req . body ;
await collection . deleteOne ({ annotationId });
// Return response in required format
res . json ({ success: true , statusCode: 200 });
const { annotationId } = req . body ;
await client . query (
'DELETE FROM reaction_annotations WHERE annotation_id = $1' ,
[ annotationId ]
);
// Return response in required format
res . json ({ success: true , statusCode: 200 });
config
Configuration for the reaction data provider.
Type: ResolverConfig . Relevant properties:
resolveTimeout: Timeout duration (in milliseconds) for resolver operations
getRetryConfig: RetryConfig . Configure retry behavior for get operations.
saveRetryConfig: RetryConfig . Configure retry behavior for save operations.
deleteRetryConfig: RetryConfig . Configure retry behavior for delete operations.
additionalFields: string[]. Specify additional fields from the ReactionAnnotation 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., commentAnnotationId, type).
const reactionResolverConfig = {
resolveTimeout: 2000 ,
getRetryConfig: { retryCount: 3 , retryDelay: 2000 },
saveRetryConfig: { retryCount: 3 , retryDelay: 2000 },
deleteRetryConfig: { retryCount: 3 , retryDelay: 2000 },
additionalFields: [ 'commentAnnotationId' , 'type' ]
};
Function based Complete Example
React / Next.js
Other Frameworks
const fetchReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/get' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const saveReactionsToDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/save' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const deleteReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/delete' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const reactionResolverConfig = {
resolveTimeout: 2000 ,
getRetryConfig: { retryCount: 3 , retryDelay: 2000 },
saveRetryConfig: { retryCount: 3 , retryDelay: 2000 },
deleteRetryConfig: { retryCount: 3 , retryDelay: 2000 },
additionalFields: [ 'commentAnnotationId' , 'type' ]
};
const reactionDataProvider = {
get: fetchReactionsFromDB ,
save: saveReactionsToDB ,
delete: deleteReactionsFromDB ,
config: reactionResolverConfig
};
< VeltProvider
apiKey = 'YOUR_API_KEY'
dataProviders = { { reaction: reactionDataProvider } }
>
</ VeltProvider >
const fetchReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/get' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const saveReactionsToDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/save' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const deleteReactionsFromDB = async ( request ) => {
const response = await fetch ( '/api/velt/reactions/delete' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request )
});
return await response . json ();
};
const reactionResolverConfig = {
resolveTimeout: 2000 ,
getRetryConfig: { retryCount: 3 , retryDelay: 2000 },
saveRetryConfig: { retryCount: 3 , retryDelay: 2000 },
deleteRetryConfig: { retryCount: 3 , retryDelay: 2000 },
additionalFields: [ 'commentAnnotationId' , 'type' ]
};
const reactionDataProvider = {
get: fetchReactionsFromDB ,
save: saveReactionsToDB ,
delete: deleteReactionsFromDB ,
config: reactionResolverConfig
};
Velt . setDataProviders ({ reaction: reactionDataProvider });
Sample Data
{
"annotationId" : "REACTION_ANNOTATION_ID" ,
"metadata" : {
"apiKey" : "API_KEY" ,
"documentId" : "DOCUMENT_ID" ,
"organizationId" : "ORGANIZATION_ID"
},
"icon" : "HEART_FACE"
}
The reaction annotation on your database stores the actual reaction icon/emoji content. {
"annotationId" : "REACTION_ANNOTATION_ID" ,
"commentAnnotationId" : "COMMENT_ANNOTATION_ID" ,
"from" : {
"userId" : "USER_ID"
},
"involvedUserIds" : [ "USER_ID" ],
"isReactionResolverUsed" : true ,
"lastUpdated" : 1768542354775 ,
"metadata" : {
"apiKey" : "API_KEY" ,
"clientDocumentId" : "DOCUMENT_ID" ,
"clientOrganizationId" : "ORGANIZATION_ID" ,
"documentId" : "INTERNAL_DOC_ID" ,
"organizationId" : "INTERNAL_ORG_ID"
},
"pageInfo" : {
"baseUrl" : "https://your-app.com" ,
"path" : "/" ,
"url" : "https://your-app.com/"
},
"reactions" : [
{
"from" : {
"userId" : "USER_ID"
},
"lastUpdated" : 1768542354775
}
],
"type" : "reaction"
}
Only reaction identifiers and metadata are stored on Velt servers. The actual reaction icon/emoji remains on your infrastructure.
Debugging
You can subscribe to dataProvider events to monitor and debug get, save, and delete operations. The event includes a moduleName field that identifies which module triggered the resolver call, helping you trace data provider requests.
You can also use the Velt Chrome DevTools extension to inspect and debug your Velt implementation.
Type: ReactionResolverModuleName
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 );
console . log ( 'Module Name:' , event . moduleName );
});
return () => subscription ?. unsubscribe ();
}, [ client ]);
const subscription = Velt . on ( 'dataProvider' ). subscribe (( event ) => {
console . log ( 'Data Provider Event:' , event );
console . log ( 'Module Name:' , event . moduleName );
});
// Unsubscribe when done
subscription ?. unsubscribe ();