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

# Comments

> Self-host your comments data while using Velt's components. Keep comment 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.
  * 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](/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations)
</Warning>

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

<Warning>
  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](/webhooks/basic) to receive events (e.g., mentions, replies), fetch the relevant comment/notification content from your database, and send emails from your own email provider.
</Warning>

# How does it work?

* When comments are created, updated, deleted or requested, the SDK uses your configured [`CommentAnnotationDataProvider`](/api-reference/sdk/models/data-models#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.

| 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 comments. The SDK automatically makes HTTP POST requests with the request body.

* Type: [`ResolverEndpointConfig`](/api-reference/sdk/models/data-models#resolverendpointconfig)
* Request body format: [`GetCommentResolverRequest`](/api-reference/sdk/models/data-models#getcommentresolverrequest)
* Response format: [`ResolverResponse<Record<string, PartialCommentAnnotation>>`](/api-reference/sdk/models/data-models#resolverresponse)

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    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>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const commentResolverConfig = {
      getConfig: {
        url: 'https://your-backend.com/api/velt/comments/get',
        headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
      }
    };

    const commentDataProvider = {
      config: commentResolverConfig
    };

    Velt.setDataProviders({ comment: commentDataProvider });
    ```
  </Tab>
</Tabs>

### saveConfig

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

* Type: [`ResolverEndpointConfig`](/api-reference/sdk/models/data-models#resolverendpointconfig)
* Request body format: [`SaveCommentResolverRequest`](/api-reference/sdk/models/data-models#savecommentresolverrequest)
* Response format: [`ResolverResponse<T>`](/api-reference/sdk/models/data-models#resolverresponse)

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    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>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const commentResolverConfig = {
      saveConfig: {
        url: 'https://your-backend.com/api/velt/comments/save',
        headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
      }
    };

    const commentDataProvider = {
      config: commentResolverConfig
    };

    Velt.setDataProviders({ comment: commentDataProvider });
    ```
  </Tab>
</Tabs>

### deleteConfig

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

* Type: [`ResolverEndpointConfig`](/api-reference/sdk/models/data-models#resolverendpointconfig)
* Request body format: [`DeleteCommentResolverRequest`](/api-reference/sdk/models/data-models#deletecommentresolverrequest)
* Response format: [`ResolverResponse<T>`](/api-reference/sdk/models/data-models#resolverresponse)

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    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>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    const commentResolverConfig = {
      deleteConfig: {
        url: 'https://your-backend.com/api/velt/comments/delete',
        headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
      }
    };

    const commentDataProvider = {
      config: commentResolverConfig
    };

    Velt.setDataProviders({ comment: commentDataProvider });
    ```
  </Tab>
</Tabs>

### Endpoint based Complete Example

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    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>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    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
    };

    Velt.setDataProviders({ comment: commentDataProvider });
    ```
  </Tab>
</Tabs>

## Function based DataProvider

Implement custom methods to handle data operations yourself.

### get

Method to fetch comments from your database. On error we will retry.

* Param: [`GetCommentResolverRequest`](/api-reference/sdk/models/data-models#getcommentresolverrequest)
* Return: [`Promise<ResolverResponse<Record<string, PartialCommentAnnotation>>>`](/api-reference/sdk/models/data-models#resolverresponse)

<Tabs>
  <Tab title="Frontend Example">
    <CodeGroup>
      ```jsx React / Next.js theme={null}
      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>
      ```

      ```js Other Frameworks theme={null}
      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,
      };

      Velt.setDataProviders({ comment: commentDataProvider });
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Backend Endpoint Example (MongoDB)">
    ```javascript theme={null}
    // Build query from request
    const { commentAnnotationIds, documentIds, organizationId } = req.body;
    const query = {};
    if (commentAnnotationIds?.length) {
      query.annotationId = { $in: commentAnnotationIds };
    }
    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 });
    ```
  </Tab>

  <Tab title="Backend Endpoint Example (PostgreSQL)">
    ```javascript theme={null}
    // Build parameterized query
    const { commentAnnotationIds, documentIds, organizationId } = req.body;
    const conditions = [];
    const values = [];
    let paramIndex = 1;

    if (commentAnnotationIds?.length) {
      conditions.push(`annotation_id = ANY($${paramIndex++})`);
      values.push(commentAnnotationIds);
    }
    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 comment_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 });
    ```
  </Tab>
</Tabs>

### save

Save comments to your database. Return a success or error response. On error we will retry.

* Param: [`SaveCommentResolverRequest`](/api-reference/sdk/models/data-models#savecommentresolverrequest)
  * Note in the `SaveCommentResolverRequest` object, you will receive [the event name](/api-reference/sdk/models/data-models#resolveractions) that triggered the save.
* Return: [`Promise<ResolverResponse<T>>`](/api-reference/sdk/models/data-models#resolverresponse)

<Warning>
  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](/api-reference/rest-apis/v2/comments-feature/comment-annotations/add-comment-annotations)
</Warning>

<Tabs>
  <Tab title="Frontend Example">
    <CodeGroup>
      ```jsx React / Next.js theme={null}
      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>
      ```

      ```js Other Frameworks theme={null}
      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,
      };

      Velt.setDataProviders({ comment: commentDataProvider });
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Backend Endpoint Example (MongoDB)">
    ```javascript theme={null}
    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 });
    ```
  </Tab>

  <Tab title="Backend Endpoint Example (PostgreSQL)">
    ```javascript theme={null}
    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 comment_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 });
    ```
  </Tab>
</Tabs>

### delete

Delete comments from your database. Return a success or error response. On error we will retry.

* Param: [`DeleteCommentResolverRequest`](/api-reference/sdk/models/data-models#deletecommentresolverrequest)
* Return: [`Promise<ResolverResponse<T>>`](/api-reference/sdk/models/data-models#resolverresponse)

<Tabs>
  <Tab title="Frontend Example">
    <CodeGroup>
      ```jsx React / Next.js theme={null}
      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>
      ```

      ```js Other Frameworks theme={null}
      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,
      };

      Velt.setDataProviders({ comment: commentDataProvider });
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Backend Endpoint Example (MongoDB)">
    ```javascript theme={null}
    const { annotationId } = req.body;

    await collection.deleteOne({ annotationId });

    // Return response in required format
    res.json({ success: true, statusCode: 200 });
    ```
  </Tab>

  <Tab title="Backend Endpoint Example (PostgreSQL)">
    ```javascript theme={null}
    const { annotationId } = req.body;

    await client.query(
      'DELETE FROM comment_annotations WHERE annotation_id = $1',
      [annotationId]
    );

    // Return response in required format
    res.json({ success: true, statusCode: 200 });
    ```
  </Tab>
</Tabs>

### config

Configuration for the comment data provider.

* Type: [`ResolverConfig`](/api-reference/sdk/models/data-models#resolverconfig). Relevant properties:
  * `resolveTimeout`: Timeout duration (in milliseconds) for resolver operations
  * `getRetryConfig`: [`RetryConfig`](/api-reference/sdk/models/data-models#retryconfig). Configure retry behavior for get operations.
  * `saveRetryConfig`: [`RetryConfig`](/api-reference/sdk/models/data-models#retryconfig). Configure retry behavior for save operations.
  * `deleteRetryConfig`: [`RetryConfig`](/api-reference/sdk/models/data-models#retryconfig). Configure retry behavior for delete operations.
  * `additionalFields`: `string[]`. Specify additional fields from the [`CommentAnnotation`](/api-reference/sdk/models/data-models#commentannotation) 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., `status`, `assignedTo`, `priority`).

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

### Function based Complete Example

<Tabs>
  <Tab title="React / Next.js">
    ```jsx theme={null}
    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>
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```js theme={null}
    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
    };

    Velt.setDataProviders({ comment: commentDataProvider });
    ```
  </Tab>
</Tabs>

# Triggering Email Notifications when Self-Hosting

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

<Steps>
  <Step title="Enable relevant webhooks">
    Subscribe to comment-related events (e.g., user mentions, replies). See [Webhooks](/webhooks/advanced).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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).
  </Step>

  <Step title="Send email via your provider">
    Use your own email service (SendGrid under your account, SES, Postmark, etc.) to send the email.
  </Step>
</Steps>

**Example:**

<Tabs>
  <Tab title="Example Webhook Payload">
    ```json cURL theme={null}
    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"
    }
    ```
  </Tab>

  <Tab title="Example Notification Code (Node.js)">
    ```javascript Node.js theme={null}
    // Pseudocode: handle webhook, fetch content, send email
    app.post('/webhooks/velt', async (req, res) => {
      const evt = req.body;
      const annotationId = evt?.commentAnnotation?.annotationId;
      const targetCommentId = evt?.targetComment?.commentId;

      // 1) Fetch self-hosted content
      const annotation = await db.commentAnnotations.get(annotationId);
      const targetComment = annotation?.comments?.[targetCommentId];

      // 2) Resolve recipients (e.g., mentioned users)
      const recipients = await resolveMentionedUsers(annotation, targetComment);

      // 3) Compose email
      const subject = `${evt.actionUser?.name} mentioned you`;
      const body = targetComment?.commentText || '';
      const pageUrl = evt?.metadata?.pageInfo?.url;

      // 4) Send via your provider
      await emailClient.send({ to: recipients, subject, html: renderTemplate({ body, pageUrl }) });

      res.sendStatus(200);
    });
    ```
  </Tab>
</Tabs>

# Sample Data

<Tabs>
  <Tab title="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>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"
                }
            }
        }
    }
    ```
  </Tab>

  <Tab title="Stored on Velt servers">
    ```json theme={null}
    {
        "annotationId": "ANNOTATION_ID",
        "annotationIndex": 3,
        "annotationNumber": 4,
        "color": "#A25F9E",
        "comments": [
            {
                "attachments": [],
                "commentId": 184639,
                "createdAt": 1768544691938,
                "from": {
                    "userId": "USER_ID"
                },
                "isCommentResolverUsed": true,
                "isCommentTextAvailable": true,
                "isDraft": false,
                "lastUpdated": "2026-01-16T05:34:19.402Z",
                "reactionAnnotationIds": ["REACTION_ANNOTATION_ID"],
                "taggedUserContacts": [],
                "to": [],
                "type": "text"
            },
            {
                "attachments": [
                    {
                        "attachmentId": 758336,
                        "bucketPath": "API_KEY/organizations/ORG_ID/docs/DOC_ID/comments/ANNOTATION_ID/758336_image.png",
                        "isAttachmentResolverUsed": true,
                        "mimeType": "image/png",
                        "size": 289870,
                        "type": "png"
                    }
                ],
                "commentId": 743772,
                "createdAt": 1768544717230,
                "from": {
                    "userId": "USER_ID"
                },
                "isCommentResolverUsed": true,
                "isDraft": false,
                "lastUpdated": "2026-01-16T05:34:31.352Z",
                "reactionAnnotationIds": [],
                "taggedUserContacts": [],
                "to": [],
                "type": "text"
            }
        ],
        "createdAt": 1768544637011,
        "from": {
            "userId": "USER_ID"
        },
        "involvedUserIds": ["USER_ID"],
        "lastUpdated": 1768542354784,
        "metadata": {
            "apiKey": "API_KEY",
            "clientDocumentId": "DOCUMENT_ID",
            "clientOrganizationId": "ORGANIZATION_ID",
            "documentId": "INTERNAL_DOC_ID",
            "organizationId": "INTERNAL_ORG_ID"
        },
        "pageInfo": {
            "baseUrl": "https://your-app.com",
            "commentUrl": "https://your-app.com/?commentId=ANNOTATION_ID",
            "path": "/",
            "url": "https://your-app.com/"
        },
        "status": {
            "id": "OPEN",
            "name": "Open",
            "color": "var(--velt-accent, #625DF5)",
            "lightColor": "var(--velt-accent-light, #E7E8FA)",
            "type": "default"
        }
    }
    ```

    Note: Comment text/HTML content is NOT stored on Velt servers when using the comment resolver. Only metadata like `isCommentResolverUsed`, `isCommentTextAvailable`, attachment references (without URL/name), and reaction annotation IDs are stored.
  </Tab>
</Tabs>

# 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](https://chromewebstore.google.com/detail/velt-devtools/nfldoicbagllmegffdapcnohakpamlnl) to inspect and debug your Velt implementation.

Type: [`CommentResolverModuleName`](/api-reference/sdk/models/data-models#commentresolvermodulename)

<Tabs>
  <Tab title="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);
        console.log('Module Name:', event.moduleName);
      });

      return () => subscription?.unsubscribe();
    }, [client]);
    ```
  </Tab>

  <Tab title="Other Frameworks">
    ```javascript theme={null}
    const subscription = Velt.on('dataProvider').subscribe((event) => {
      console.log('Data Provider Event:', event);
      console.log('Module Name:', event.moduleName);
    });

    // Unsubscribe when done
    subscription?.unsubscribe();
    ```
  </Tab>
</Tabs>
