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.
  • If you are using REST API to add or update comments, ensure that you set isCommentResolverUsed and isCommentTextAvailable fields in the request object. Learn more

Overview

Velt supports self-hosting your comments and related data:
  • Comments can be stored on your own infrastructure, with only necessary identifiers on Velt servers.
  • Velt Components automatically hydrate comment data in the frontend by fetching from your configured data provider.
  • This gives you full control over comment data while maintaining all Velt collaboration features.
  • This automatically also ensures that the in-app notifications content is not stored on Velt servers. The content is generated using the comments data in the frontend.
Email notifications via Velt’s SendGrid integration are not available when you self-host comment content. Since the content lives on your infrastructure, Velt cannot construct and send emails via the sendgrid integration. Instead, use Webhooks to receive events (e.g., mentions, replies), fetch the relevant comment/notification content from your database, and send emails from your own email provider.

How does it work?

  • When comments are created, updated, deleted or requested, the SDK uses your configured CommentAnnotationDataProvider to handle storage and retrieval
  • The data provider implements get, save, and delete methods to interact with your database
  • Velt handles the data mapping and realtime synchronization while delegating persistence of actual content to your infrastructure
  • The data provider works at the Comment Annotation (Thread) level not at the individual Comment (Message) level.
  • For write requests (save, delete), the operation is first performed on your database and only if we get a success response, the SDK will perform the operation on the Velt server. If the operation fails on your database, the SDK will not perform the operation on the Velt server.
  • You can configure retries, timeouts, etc. for the data provider.

Implementation Approaches

You can implement comment self-hosting using either of these approaches:
  1. Endpoint based: Provide endpoint URLs and let the SDK handle HTTP requests
  2. Function based: Implement get, 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 endpoint url, static or async headers, and optional credentials
FlexibilityHighMedium
SpeedMediumHigh

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 comments. The SDK automatically makes HTTP POST requests with the request body.
const commentResolverConfig = {
  getConfig: {
    url: 'https://your-backend.com/api/velt/comments/get',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const commentDataProvider = {
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

saveConfig

Config-based endpoint for saving comments. The SDK automatically makes HTTP POST requests with the request body. Set additionalSaveEvents in your comment resolver config to opt into non-core save events, such as status changes, priority changes, assignments, approvals, comment-level reactions, and subscription changes.
const commentResolverConfig = {
  saveConfig: {
    url: 'https://your-backend.com/api/velt/comments/save',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const commentDataProvider = {
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

deleteConfig

Config-based endpoint for deleting comments. The SDK automatically makes HTTP POST requests with the request body.
const commentResolverConfig = {
  deleteConfig: {
    url: 'https://your-backend.com/api/velt/comments/delete',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const commentDataProvider = {
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

Endpoint based Complete Example

const commentResolverConfig = {
  getConfig: {
    url: 'https://your-backend.com/api/velt/comments/get',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },
  saveConfig: {
    url: 'https://your-backend.com/api/velt/comments/save',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },
  deleteConfig: {
    url: 'https://your-backend.com/api/velt/comments/delete',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },
  resolveTimeout: 2000,
  getRetryConfig: { retryCount: 3, retryDelay: 2000 },
  saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
  deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
  additionalFields: ['status', 'assignedTo', 'priority']
};

const commentDataProvider = {
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

Async headers and credentials

Endpoint configs (getConfig, saveConfig, deleteConfig) support an async headers function for short-lived per-request tokens, and a credentials field for cookie/session auth. This applies to all endpoint-based providers — see Async headers and credentials for details and examples.

additionalSaveEvents

By default the save endpoint receives only the 4 core PII events (COMMENT_ANNOTATION_ADD, COMMENT_ADD, COMMENT_UPDATE, COMMENT_DELETE). Opt in per-config via additionalSaveEvents to also receive non-core, annotation-level lifecycle events (e.g. status, assignment, approval, reactions) on the same save endpoint. The event values come from CommentResolverSaveEvent.
import { CommentResolverSaveEvent } from '@veltdev/react';

const commentResolverConfig = {
  additionalSaveEvents: [
    { event: CommentResolverSaveEvent.STATUS_CHANGE },
    { event: CommentResolverSaveEvent.ASSIGN },
    { event: CommentResolverSaveEvent.APPROVE },
    { event: CommentResolverSaveEvent.REACTION_ADD }
  ],
  saveConfig: {
    url: 'https://your-backend.com/api/velt/comments/save',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const commentDataProvider = {
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>
Behavior nuances:
  • Additional events bypass the PII-change dedup gate — they are sent even when no PII changed.
  • The same stripped PartialCommentAnnotation payload is sent, so there is no new data egress.
  • For annotation-level events, commentId and targetComment are omitted (no specific target comment). For reaction events (comment.reaction_add / comment.reaction_delete), both are included since the reaction is on a specific comment.

Function based DataProvider

Implement custom methods to handle data operations yourself.

get

Method to fetch comments from your database. On error we will retry.
const fetchCommentsFromDB = async (request) => {
  const response = await fetch('/api/velt/comments/get', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const commentDataProvider = {
  get: fetchCommentsFromDB,
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

save

Save comments to your database. Return a success or error response. On error we will retry.
  • Param: SaveCommentResolverRequest
    • Note in the SaveCommentResolverRequest object, you will receive the event name that triggered the save. Core values use ResolverActions (the 4 core PII events); opt-in non-core values, enabled via additionalSaveEvents, use CommentResolverSaveEvent or a raw string.
    • The payload also includes an optional targetComment (the full PartialComment for the comment identified by commentId), so your backend no longer needs to re-derive it by scanning commentAnnotation[*].comments. Use it as request context in your handler; saveComments does not persist it automatically. It is omitted when commentId is absent or unmatched.
  • Return: Promise<ResolverResponse<T>>
If you are using REST API to add or update comments, ensure that you set isCommentResolverUsed and isCommentTextAvailable fields in the request object. Learn more
const saveCommentsToDB = async (request) => {
  const response = await fetch('/api/velt/comments/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const commentDataProvider = {
  save: saveCommentsToDB,
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

delete

Delete comments from your database. Return a success or error response. On error we will retry.
const deleteCommentsFromDB = async (request) => {
  const response = await fetch('/api/velt/comments/delete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const commentDataProvider = {
  delete: deleteCommentsFromDB,
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

config

Configuration for the comment 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[]. Custom fields that are copied into the resolver request payloads sent to your backend while still being kept in Velt’s database. Use this when you want a copy of a field in your own backend (e.g., for analytics or search) without removing it from Velt — for example status, assignedTo, priority.
    • fieldsToRemove: string[]. Custom fields that are moved out of Velt’s database into yours: they are sent to your backend, deleted from Velt’s storage, and merged back on read. Use this for custom fields that must not be stored by Velt at all (e.g., internalTicketId). See Excluding & extending fields for the full behavior comparison.
    • additionalSaveEvents: CommentResolverSaveEvent[]. Additional non-core comment events to send to your save resolver, such as status changes, priority changes, assignment changes, approvals, comment-level reactions, and subscription changes.
fieldsToRemove is only for your own custom fields. Never list a field Velt uses to query, scope, position, sync, or render an annotation (e.g., metadata, annotationId, status, location, from, isCommentResolverUsed) or comments will fail to load, appear in the wrong place, or break filtering. If in doubt, use additionalFields instead. See the full list of fields to avoid.
const commentResolverConfig = {
  resolveTimeout: 2000,
  getRetryConfig: { retryCount: 3, retryDelay: 2000 },
  saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
  deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
  additionalFields: ['status', 'assignedTo', 'priority'],
  fieldsToRemove: ['internalTicketId'],
  additionalSaveEvents: [
    'comment_annotation.status_change',
    'comment_annotation.priority_change',
    'comment_annotation.assign',
    'comment.reaction_add',
    'comment.reaction_delete'
  ]
};

Function based Complete Example

const fetchCommentsFromDB = async (request) => {
  const response = await fetch('/api/velt/comments/get', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const saveCommentsToDB = async (request) => {
  const response = await fetch('/api/velt/comments/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const deleteCommentsFromDB = async (request) => {
  const response = await fetch('/api/velt/comments/delete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  return await response.json();
};

const commentResolverConfig = {
  resolveTimeout: 2000,
  getRetryConfig: { retryCount: 3, retryDelay: 2000 },
  saveRetryConfig: { retryCount: 3, retryDelay: 2000 },
  deleteRetryConfig: { retryCount: 3, retryDelay: 2000 },
  additionalFields: ['status', 'assignedTo', 'priority']
};

const commentDataProvider = {
  get: fetchCommentsFromDB,
  save: saveCommentsToDB,
  delete: deleteCommentsFromDB,
  config: commentResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ comment: commentDataProvider }}
>
</VeltProvider>

Triggering Email Notifications when Self-Hosting

When you self-host content, use Webhooks to trigger emails from your own system:
1

Enable relevant webhooks

Subscribe to comment-related events (e.g., user mentions, replies). See Webhooks.
2

Receive webhook and fetch content from your DB

Your server receives the webhook event. Use IDs from the payload (e.g., annotationId, commentId) to query your own comment and notification content from your database via your Data Provider.
3

Assemble email content and recipients

Combine the webhook event context with the self-hosted content to build the subject, body, and list of recipients (e.g., mentioned users).
4

Send email via your provider

Use your own email service (SendGrid under your account, SES, Postmark, etc.) to send the email.
Example:
cURL
POST /webhooks/velt HTTP/1.1
Content-Type: application/json

{
  "webhookId": "webhook-123",
  "actionType": "newlyAdded",
  "commentAnnotation": {
    "annotationId": "ANNOTATION_ID",
    "metadata": {
      "documentId": "DOC_ID",
      "apiKey": "API_KEY"
    }
  },
  "targetComment": {
    "commentId": 123,
    "from": {
      "userId": "USER_1"
    }
  },
  "actionUser": {
    "userId": "USER_1",
    "name": "John Doe",
    "email": "john@example.com"
  },
  "metadata": {
    "apiKey": "API_KEY",
    "documentId": "DOC_ID",
    "pageInfo": {
      "url": "https://app.example.com/doc/123"
    }
  },
  "notificationSource": "comment",
  "platform": "sdk"
}

Sample Data

{
    "annotationId": "ANNOTATION_ID",
    "metadata": {
        "apiKey": "API_KEY",
        "documentId": "DOCUMENT_ID",
        "organizationId": "ORGANIZATION_ID"
    },
    "comments": {
        "184639": {
            "commentId": 184639,
            "commentHtml": "<p>Hey @Jane, can you review this?</p>",
            "commentText": "Hey @Jane, can you review this?",
            "from": {
                "userId": "USER_ID"
            },
            "to": [
                {
                    "userId": "JANE_USER_ID"
                }
            ],
            "taggedUserContacts": [
                {
                    "userId": "JANE_USER_ID",
                    "contact": {
                        "userId": "JANE_USER_ID"
                    },
                    "text": "@Jane"
                }
            ]
        },
        "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"
            }
        }
    }
}

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: CommentResolverModuleName
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]);