- Velt keeps the structural / non-PII data — IDs, document and organization references, locations and targets, statuses, timestamps, and relationships. This is what Velt needs to position pins, thread comments, drive real-time sync, and render the UI shell.
- You keep the content / PII — comment text, user information, transcripts, attachment files, and any custom fields — inside your own database and/or file storage.
get, save, delete) to interact with your storage, and Velt Components automatically hydrate the data in the frontend by fetching from your providers.
This approach gives you complete control and ownership of your data while maintaining all Velt collaboration features and real-time functionality.
How it works
Velt uses a strip-on-write, merge-on-read model:- On write (a comment is added, a recording is saved, …): Velt strips the PII out of the record, writes only the structural remainder to Velt’s database, and hands the stripped PII to your provider to persist.
- On read (loading a document, receiving a real-time update, …): Velt calls your provider to fetch the PII back, then merges it into the structural record so the UI renders exactly as it would in a fully Velt-hosted setup.
save, delete), the operation is performed on your database first. Only on a success response does Velt apply the change on its own servers. If the operation fails on your side, Velt does not change its own data and retries if you have configured retries.
Supported features at a glance
| Feature | Provider key | get | save | delete | Model |
|---|---|---|---|---|---|
| Users | user | ✅ | — | — | Maps userId → user object |
| Comments | comment | ✅ | ✅ | ✅ | Strip on write, merge on read |
| Reactions | reaction | ✅ | ✅ | ✅ | Strip on write, merge on read |
| Attachments | attachment | — | ✅ | ✅ | File storage only |
| Recordings | recorder | ✅ | ✅ | ✅ | Strip on write + file storage modes |
| Activity | activity | ✅ | ✅ | — | Append-only (no delete) |
| Notifications | notification | ✅ | — | ✅ | Read-only enrichment (custom notifications only) |
| Anonymous users | anonymousUser | resolve-by-email | — | — | Maps email → userId |
Mix and match. You can self-host some features and let Velt host the rest. If you don’t register a provider for a feature, that feature stays fully Velt-hosted.
Registering data providers
All providers are registered through a single method,setDataProviders(), which accepts a VeltDataProvider object. Every key is optional, so you only pass the providers you actually want to self-host.
- React / Next.js
- Other Frameworks
setAnonymousUserDataProvider(), if you prefer to register it separately.
Two ways to implement: callback or URL
Each provider operation (get / save / delete) can be satisfied in one of two ways:
- A callback function — you implement the method directly in your frontend code.
- A config URL — you point Velt at an HTTP endpoint (
config.getConfig.url,config.saveConfig.url,config.deleteConfig.url) and Velt makes the request for you.
get callback but a saveConfig.url).
| 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 |
The provider contract
All providers share a commonconfig object and a common response shape.
config (ResolverConfig)
Type:ResolverConfig.
| Field | Type | Purpose |
|---|---|---|
resolveTimeout | number (ms) | Max time Velt waits for a read before giving up. Default: 60,000 ms (1 minute). |
getRetryConfig | RetryConfig | Retry policy for read (get) calls. |
saveRetryConfig | RetryConfig | Retry policy for save calls. |
deleteRetryConfig | RetryConfig | Retry policy for delete calls. |
getConfig | ResolverEndpointConfig | HTTP endpoint for reads (URL style). |
saveConfig | ResolverEndpointConfig | HTTP endpoint for saves (URL style). |
deleteConfig | ResolverEndpointConfig | HTTP endpoint for deletes (URL style). |
additionalFields | string[] | Custom fields to copy into your DB while keeping them in Velt’s. See Excluding & extending fields. |
fieldsToRemove | string[] | Custom fields to move out of Velt’s DB into yours. See Excluding & extending fields. |
RetryConfig supports retryCount (how many times to retry on failure), retryDelay (delay between retries in ms), and revertOnFailure (roll back Velt’s optimistic local change if your call fails).
The notification provider uses a slightly reduced config,
NotificationResolverConfig, with only resolveTimeout, getRetryConfig, deleteRetryConfig, getConfig, and deleteConfig. It does not support fieldsToRemove / additionalFields.Response shape (ResolverResponse)
Every callback and every HTTP endpoint must return / respond with theResolverResponse<T> shape:
statusCode: 200 means success. Any other status triggers Velt’s error handling and, if revertOnFailure is set, rolls back the optimistic local update.
HTTP endpoint contract (URL style)
When you use a config URL, Velt issues:- A
POSTto yoururl Content-Type: application/jsonplus anyheadersyou supplied- A request body equal to the JSON request object for that operation (the same object your callback would receive)
2xx and a JSON body of { "data": … } for reads (the data is unwrapped into the resolver response). For saves and deletes, a 2xx is enough.
Attachment and recording uploads are the exception — they use
multipart/form-data rather than JSON, because a binary file is involved. See Attachment & recording storage.Metadata your provider receives
Everyget / save / delete request includes a metadata object (BaseMetadata) describing the organization/document context, so you can scope the data correctly in your backend. Velt sends you a client-facing subset: it maps Velt’s internal IDs back to the IDs you originally passed to Velt and removes purely internal bookkeeping fields.
| Field | Meaning |
|---|---|
apiKey | Your Velt API key. |
documentId | Your document ID (the one you supplied to Velt). |
organizationId | Your organization ID (the one you supplied to Velt). |
folderId | Folder ID, when present. |
documentMetadata | Your document metadata, when present. |
sdkVersion | The SDK version that produced the event. |
documentId and organizationId are your IDs, not Velt’s internal hashed IDs. Use them directly as keys in your own database. Internal bookkeeping fields (e.g., clientDocumentId, clientOrganizationId, veltFolderId, pageInfo) are not sent to your provider.delete operations, Velt sends only the bare minimum needed to locate the record:
Per-feature reference
Each subsection shows the provider interface and the request types. The exact field inventory for what gets stored where is consolidated in the Complete field inventory.Comments (comment)
PartialCommentAnnotation.
Example — callback get / save / delete:
Reactions (reaction)
reactionAnnotationIds instead of commentAnnotationIds). The PII payload is PartialReactionAnnotation. See Reactions.
Recordings (recorder)
PartialRecorderAnnotation. Recordings are special because of file storage — see Attachment & recording storage and Recordings.
Notifications (notification)
save. It only applies to custom notifications (notifications whose source is custom — i.e., ones you created via the notifications API). Standard comment/recording notifications are unaffected. The PII payload is PartialNotification. See Notifications.
Activity (activity)
delete. A single activity record can contain PII from several features at once (comment text, reaction icons, recording transcripts); Velt resolves those through the respective feature providers and through the activity provider in turn. The PII payload is PartialActivityRecord. See Activity.
Attachments (attachment)
get — the stored file’s url is kept inside the comment/recording payload. See Attachment & recording storage and Attachments.
Anonymous users (anonymousUser)
resolveUserIdsByEmail to map those emails to your userIds before the comment is persisted. You return a { email: userId } map (statusCode: 200). Velt then backfills the resolved userId into the comment (rewriting the mention text to reference the userId and dropping the raw email). This stores no separate records — it’s a just-in-time identity resolution step. See Anonymous User Resolution.
Users (user, optional)
userIds into full user objects (name, email, avatar) from your own user directory, so user PII never has to live in Velt. It is the data source that all the other resolvers rely on when they strip user objects down to { userId }. See Users.
Complete field inventory
For an exhaustive, ground-truthed breakdown of every persisted field — with types, example values, descriptions, and per-feature strip rules — covering both sides of the split (Velt’s DB and your DB) and expanding every nested structural object into its own sub-table, see the dedicated reference:Complete Field Inventory
Every persisted field for comments, reactions, recordings, notifications, activity, and attachments — Velt’s DB vs. your DB, with types, examples, and notes.
- User objects are reduced to
{ userId }in Velt’s DB for most features (comments, recordings, activity). The full user object (name, email, avatar, …) lives in your backend, resolved via youruserprovider. It is the data source that all the other resolvers rely on when they strip user objects down to{ userId }. - The client-facing
metadata(see above) accompanies every PII payload you store. - Velt sets an internal flag on the structural record (e.g.,
isCommentResolverUsed,isReactionResolverUsed,isRecorderResolverUsed,isNotificationResolverUsed,isActivityResolverUsed) so the UI knows to wait for resolver data. You don’t need to store these.
Attachment & recording storage
Attachments and recordings involve binary files, not just JSON records. To keep them on your own infrastructure, provide a storage provider: Velt hands you the rawFile, you upload it to your bucket (S3, GCS, Azure Blob, …), and return { url }. Velt never touches the bytes. Implement it as an AttachmentDataProvider — save / delete callbacks, or saveConfig / deleteConfig URLs.
Storage scopes
Comment attachments and recording files are configured separately, so you can route them to different destinations:| Scope | Configured via | Used for |
|---|---|---|
| Comment attachments | dataProviders.attachment | Files attached to comments. |
| Recording files | dataProviders.recorder.storage | Video/audio recording files. |
The upload contract (storage providers)
Storage providers are the one place the contract differs from the JSON model, because a binary file is involved. Callback style — yoursave receives a ResolverAttachment:
multipart/form-data (not JSON) to your saveConfig.url:
- a
filepart containing the raw file, and - a
requestpart containing JSON:{ attachment: { attachmentId, name, mimeType }, metadata, event }.
{ "data": { "url": "https://…" } }. (Velt deliberately omits Content-Type so the browser sets the correct multipart boundary.)
Deletes (both scopes) send the minimized metadata { apiKey, documentId, organizationId, folderId? } plus the attachmentId, so you can remove the right file.
How recording files are uploaded
When a recording storage provider (recorder.storage) is set, Velt uploads the entire recording to your storage once — after the recording stops and the annotation is saved — and then patches the returned file URL onto the annotation. Velt also skips its own server-side encoding/transcription post-processing in this case; you own those files end to end.
See Recordings and Attachments for full examples.
Excluding & extending fields
Velt already strips its built-in PII automatically (comment text, user info, transcripts, …) — you don’t configure that.fieldsToRemove and additionalFields are for your own custom fields that you attach to an annotation and want handled a particular way. Both are set on the provider’s config.
fieldsToRemove— data sovereignty (move). Each listed field is copied into the payload sent to your backend and deleted from Velt’s database. On read, Velt restores the field from your backend and merges it back into the record. Use this when a custom field must not be stored by Velt at all.additionalFields— replication (copy). Each listed field is deep-copied into the payload sent to your backend, but kept in Velt’s database. On read there is nothing to merge (the field is already present). 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.
| Aspect | fieldsToRemove | additionalFields |
|---|---|---|
| Effect on Velt’s DB | Removed | Kept |
| Sent to your backend | Yes (moved) | Yes (copied) |
| Merged back on read | Yes (restored from you) | No (already in Velt’s DB) |
Falsy values (0, "", false) | Copied only if truthy | Preserved |
| Processing order | First | Second |
| If a field is in both lists | fieldsToRemove wins (removed first) | — |
Where these are supported
| Provider | fieldsToRemove | additionalFields |
|---|---|---|
comment | ✅ | ✅ |
reaction | — | ✅ |
recorder | — | ✅ |
activity | ✅ (for custom activity types) | — |
notification | — | — |
Worked example
Store a custompriorityScore only in your own database:
priorityScore is never written to Velt’s database — it lives only in your db.comments and is merged back into the comment whenever Velt loads it.
Operational behavior
- Timeouts. Reads are wrapped in a timeout (default 1 minute, configurable via
config.resolveTimeout). On timeout, Velt proceeds gracefully and renders whatever structural data it already has — it never blocks the UI indefinitely. - Retries.
retryCount/retryDelaylet you retry transient failures. Combine withrevertOnFailureto roll back Velt’s optimistic local change if your save/delete ultimately fails. - Degrade, don’t drop. If resolution fails, Velt keeps the structural record and renders it without the PII rather than dropping it. Your data is never lost because a resolver call failed.
- Not every update calls
save. Velt only sends asavewhen the PII actually changed and the action maps to a meaningful resolver event (add/update/delete of the comment body, etc.). Pure structural changes (status, priority, assignment) are handled by Velt and won’t necessarily call your save. - Custom notifications only. The notification resolver applies only to notifications whose source is
custom. Standard comment/recording notifications are resolved through their own feature providers. See Notifications. - Cross-organization notifications. “For You” notifications can come from organizations other than the active one. Velt calls your notification/comment
getendpoints with each entry’s ownorganizationId— make sure your endpoints honor theorganizationIdin the request rather than assuming the current org. - Activity is multi-feature. A single activity record may contain PII owned by several providers. Velt resolves them in sequence (user → comment → reaction → recorder → activity), so register all relevant providers for activity feeds to render fully.
Backend implementation
For endpoint (URL) mode, Velt provides official server SDKs that handle request parsing, payload types, and response formatting for every resolver operation:Node SDK
Self-hosting backend with built-in MongoDB + AWS S3 support, or call Velt’s REST APIs directly.
Python SDK
Server-side SDK for self-hosting backend implementation and REST API access.
End-to-end example
A consolidated setup self-hosting comments (with a custom field moved out), recordings (own storage), custom notifications, activity, and anonymous-user resolution:comment callbacks with:
Quick reference
Provider cheat-sheet
| Provider key | Methods | Request type(s) | Payload (your DB) | fieldsToRemove | additionalFields |
|---|---|---|---|---|---|
user | get | userIds: string[] | Record<userId, User> | — | — |
comment | get / save / delete | Get/Save/DeleteCommentResolverRequest | PartialCommentAnnotation | ✅ | ✅ |
reaction | get / save / delete | Get/Save/DeleteReactionResolverRequest | PartialReactionAnnotation | — | ✅ |
attachment | save / delete | Save/DeleteAttachmentResolverRequest | binary file → { url } | — | ✅ |
recorder | get / save / delete (+ storage) | Get/Save/DeleteRecorderResolverRequest | PartialRecorderAnnotation | — | ✅ |
activity | get / save | Get/SaveActivityResolverRequest | PartialActivityRecord | ✅ (custom types) | — |
notification | get / delete | Get/DeleteNotificationResolverRequest | PartialNotification | — | — |
anonymousUser | resolveUserIdsByEmail | ResolveUserIdsByEmailRequest | { email: userId } (transient) | — | — |
Defaults & conventions
- Success =
statusCode: 200. Anything else = failure. - Read timeout = 60,000 ms (1 minute) unless overridden.
- Retries default to 0; set
retryCount/retryDelayto enable. - Delete metadata is always
{ apiKey, documentId, organizationId, folderId? }. - JSON endpoints:
POST,application/json, respond{ "data": … }for reads. - File upload endpoints:
POST,multipart/form-data(file+requestparts), respond{ "data": { "url": … } }. - Storage scopes:
dataProviders.attachment(comment files) anddataProviders.recorder.storage(recording files) are configured independently.
Supported infrastructure
You can self-host your data on any infrastructure you want, as long as you can receive and return the data in the provided format. Here are some examples:- AWS
- GCP
- Azure
- Any Custom Infrastructure
Debugging
You can subscribe todataProvider events to monitor and debug get, save, and delete operations across all providers. 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.
- React / Next.js
- Other Frameworks

