Skip to main content
  • This is currently only compatible with the setDocuments method.
  • Ensure that the data providers are set prior to calling the 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 activity log PII data:
  • Activity content (comment text embedded in change history), feature-specific entity snapshots, and custom fields can be stored on your own infrastructure, with only necessary identifiers on Velt servers.
  • Velt components automatically re-hydrate activity data in the frontend by fetching from your configured data provider.
  • This gives you full control over PII while maintaining all Velt activity log features.

How does it work?

When activity records are created or read:
  1. The SDK uses your configured ActivityAnnotationDataProvider to handle storage and retrieval.
  2. Your data provider implements two optional methods, each of which can be supplied as either a callback function or a config endpoint URL:
    • get / getConfig: Fetches activity PII from your database and returns it for re-hydration
    • save / saveConfig: Stores PII fields stripped from the activity record before Velt writes to its backend
  3. Each method is considered valid as long as it has either a callback function (get / save) or a corresponding config endpoint URL (getConfig / saveConfig).
The process works as follows: On write (stripActivityPII):
  1. The SDK strips PII from the activity record. How much is stripped depends on featureType — see What gets stripped below.
  2. Your save handler receives the stripped PII (PartialActivityRecord) and stores it in your backend.
  3. Velt’s backend stores the skeleton record (structural identifiers, timestamps, flags) plus any non-PII fields.
On read (rehydrateActivities):
  1. The SDK fetches the activity records from Velt’s backend (with PII absent).
  2. The SDK runs 5 resolver passes in order: usercommentreactionrecorderactivity. Your get handler is called during the activity pass with the activity IDs.
  3. The returned PII is merged back into the activity records before they are delivered to your UI.
  4. ActivityRecord.isActivityResolverUsed is set to true when PII was stripped by the resolver. Use this to show a loading state while re-hydration is pending.

What gets stripped

How much PII is removed from entityData and entityTargetData depends on featureType.

Built-in featureType (comment / reaction / recorder) — partial strip

entityData and entityTargetData objects are kept on the Velt record; only specific PII fields inside them are removed. Structural identifiers (annotation IDs, comment IDs, target IDs) are preserved so the read path can re-match resolved data.
FeatureFields removed from entityData / entityTargetDataGating
commentcommentText, commentHtml, attachments (name/url), from, to, taggedUserContactsAlways (when the activity resolver is active)
reactionicon, from, metadataOnly when both the activity resolver and the reaction resolver are active. Otherwise only user objects are reduced to { userId } and the rest survives.
recorderfrom, transcription, attachment, attachmentsOnly when both the activity resolver and the recorder resolver are active. Otherwise only user objects are reduced to { userId } and the rest survives.
changes['commentText'] is moved to your DB only when the activity resolver is active. If only a comment resolver is registered (no activity resolver), it is preserved on the Velt side to avoid unrestorable loss. Comment entityData / entityTargetData PII is handled by the comment resolver’s own store, not duplicated through the activity resolver.

featureType === 'custom' — wholesale removal by config

There is no automatic field-level stripping for custom activities. The only fields removed are the top-level keys you list in config.fieldsToRemove — each listed key is moved wholesale to your DB and deleted from the Velt record. On read it’s restored by wholesale replacement.
  • If you list entityData or entityTargetData in fieldsToRemove, the entire field disappears from the Velt record (not field-by-field).
  • If you don’t list them, they’re left untouched on the Velt record.
  • fieldsToRemove is ignored for built-in feature types.

Universal

  • displayMessage is always recomputed on the client and stored in neither DB.
  • actionUser and any user objects in changes and displayMessageTemplateData are reduced to { userId } whenever a user resolver is active.
  • Activity is append-only — there is no delete.

Implementation

Implementation Approaches

You can implement activity 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 and save 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.
headers may also be an async function resolved per request, and credentials ('include' | 'same-origin' | 'omit') enables cookie/session auth — see Async headers and credentials for details.
Activity is append-only — there is no delete operation, so there is no deleteConfig for activity.

getConfig

Config-based endpoint for fetching activity PII. The SDK automatically makes HTTP POST requests with the request body.
const activityResolverConfig = {
  getConfig: {
    url: 'https://your-backend.com/api/velt/activity/get',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const activityDataProvider = {
  config: activityResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ activity: activityDataProvider }}
>
</VeltProvider>

saveConfig

Config-based endpoint for saving stripped activity PII. The SDK automatically makes HTTP POST requests with the request body.
const activityResolverConfig = {
  saveConfig: {
    url: 'https://your-backend.com/api/velt/activity/save',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  }
};

const activityDataProvider = {
  config: activityResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ activity: activityDataProvider }}
>
</VeltProvider>

Endpoint based Complete Example

const activityResolverConfig = {
  getConfig: {
    url: 'https://your-backend.com/api/velt/activity/get',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },
  saveConfig: {
    url: 'https://your-backend.com/api/velt/activity/save',
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  },
  resolveTimeout: 60000,
  getRetryConfig: { retryCount: 3, retryDelay: 2000 },
  saveRetryConfig: { retryCount: 3, retryDelay: 2000, revertOnFailure: true },
  fieldsToRemove: ['customSensitiveField']
};

const activityDataProvider = {
  config: activityResolverConfig
};

<VeltProvider
  apiKey='YOUR_API_KEY'
  dataProviders={{ activity: activityDataProvider }}
>
</VeltProvider>

Function based DataProvider

Implement custom get and save methods to handle data operations yourself.

get

Fetch activity PII from your database. Called when activity records need to be re-hydrated in the frontend.
const activityDataProvider = {
  get: async (request) => {
    const response = await fetch('/api/velt/activity/get', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request),
    });
    return await response.json();
  },
};

client.setDataProviders({ activity: activityDataProvider });

save

Store activity PII fields stripped before Velt writes to its backend. Called when an activity record is created or updated.
const activityDataProvider = {
  save: async (request) => {
    const response = await fetch('/api/velt/activity/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request),
    });
    return await response.json();
  },
};

client.setDataProviders({ activity: activityDataProvider });

config

Configuration for the activity 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. RetryConfig is { retryCount?: number; retryDelay?: number; revertOnFailure?: boolean }. Note: revertOnFailure reverts the optimistic cache update if the save ultimately fails.
    • getConfig: ResolverEndpointConfig. Endpoint URL + headers for fetching activity PII. See Endpoint based DataProvider.
    • saveConfig: ResolverEndpointConfig. Endpoint URL + headers for saving stripped activity PII. See Endpoint based DataProvider.
    • fieldsToRemove: string[]. Top-level keys to move wholesale from the activity record to your DB. Applies only to featureType === 'custom' — built-in feature types (comment / reaction / recorder) ignore this and use the feature-aware partial strip described in What gets stripped. Listing entityData or entityTargetData moves the entire field (not field-by-field).
const activityResolverConfig = {
  resolveTimeout: 60000,
  getRetryConfig: { retryCount: 3, retryDelay: 2000 },
  saveRetryConfig: { retryCount: 3, retryDelay: 2000, revertOnFailure: true },
  fieldsToRemove: ['customSensitiveField']
};

Function based Complete Example

// Using API methods
client.setDataProviders({
  activity: {
    get: async (request) => {
      const response = await fetch('/api/velt/activity/get', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request),
      });
      return await response.json();
    },
    save: async (request) => {
      const response = await fetch('/api/velt/activity/save', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request),
      });
      return await response.json();
    },
    config: {
      resolveTimeout: 60000,
      fieldsToRemove: ['customSensitiveField'],
    },
  },
});

Loading State

Use isActivityResolverUsed on ActivityRecord to show a loading state while PII is being fetched from your backend:
const activities = useAllActivities();

activities?.map((activity) => (
  activity.isActivityResolverUsed
    ? <ActivitySkeleton key={activity.id} />
    : <ActivityItem key={activity.id} activity={activity} />
));

Sample Data

{
  "id": "activityId",
  "metadata": {
    "apiKey": "API_KEY",
    "documentId": "DOCUMENT_ID",
    "organizationId": "ORGANIZATION_ID"
  },
  "entityData": {
    "prId": "pr-123",
    "title": "Add dark mode",
    "secretKey": "sk-abc"
  },
  "entityTargetData": {
    "commitSha": "abc123",
    "branch": "main"
  },
  "displayMessageTemplate": "{{actor.name}} deployed {{version}} to {{env}}",
  "displayMessageTemplateData": {
    "actor": { "userId": "user-1", "name": "Alice", "email": "alice@example.com" },
    "version": "v2.3.1",
    "env": "production"
  }
}