Skip to main content
Companion to the Self-Hosting Overview. That guide’s field-inventory section lists the field names split between Velt’s DB and your DB. This page adds Type, Example value, Description, and Notes for every field, covers both sides of the split, and expands every nested structural object into its own sub-table. Ground-truthed against the SDK models and the resolver strip logic, not just the guide.

How to read this

When a customer registers a data provider (Velt.setDataProviders({ … })), the split happens on the frontend, before anything is written:
  • Velt’s DB keeps the structural remainder — IDs, positions, locations/targets, statuses, timestamps, relationships, and flags.
  • Your DB receives the Partial<X> PII payload the SDK strips on the device and hands to your provider’s save (or saveConfig.url) — then merges back on read so the UI renders identically to a fully-Velt-hosted setup.

🔒 Privacy guarantee

Anything not stored in Velt’s DB never leaves the frontend device for Velt’s network — not in transit, not at rest. The PII is stripped on the device before any request to Velt is made and is sent only to your DB (or recomputed entirely on the client). Velt never receives it, transmits it, or stores it.

Note vocabulary

TermMeaning
keptSent to Velt and written to its DB verbatim.
reducedUser object collapsed to { userId } before any write — only the userId is sent to Velt; the rest of the user object (name / email / avatar) is never sent to Velt.
never sent to VeltThe field’s data is stripped on the frontend before any write, so it never leaves the device for Velt — not in transit, not at rest. It goes only to your DB (when applicable) or is recomputed on the client. (For a few fields the key is still written to Velt as null / {} — only the value is withheld.)
copied-not-movedSent to both your DB and Velt’s DB (replicated, not relocated).
@deprecatedStill stored for back-compat; do not rely on it.
Features covered: comment, reaction, recorder, notification, activity, attachment (+ shared building blocks). Features without a self-hosting data-provider split — cursor, presence, huddle, selection, tag, area, arrow, rewriter, standalone transcription — are not included; they have no resolver and stay fully Velt-hosted.

Table of Contents

  1. Comments
  2. Reactions
  3. Recordings
  4. Notifications
  5. Activity
  6. Attachments
  7. Shared building blocks
  8. Summary matrix & field counts

1. Comments (comment)

Model: CommentAnnotation. PII payload: PartialCommentAnnotation.

1.A — Stored in Velt’s DB (CommentAnnotation, structural remainder)

FieldTypeExampleDescriptionNotes
annotationIdstring"a8f3c2e1-9b4d-4e7a-8c1f-2d3e4f5a6b7c"Unique id for the comment pin annotation.auto-generated; join key (also in your DB)
annotationNumbernumber42Sequential pin number.auto-generated
visibilityConfigCommentAnnotationVisibilityConfig { type; organizationId?; userIds?[] }{ type: 'private', userIds: ['u_1','u_2'] }Who can see the annotation (public/private/org/user-list).kept
commentsComment[][{ commentId: 482910, type: 'text', from: { userId: 'u_1' } }]All comments in the thread.kept; per-comment PII never sent to Velt — see Comment sub-table
commentCategoriesCustomCategory[][{ id: 'bug', name: 'Bug', color: '#f00' }]Categories the annotation belongs to.defaults []
fromUser{ userId }{ userId: 'u_1' }Creator of the annotation.reduced when user provider active; copied-not-moved
colorstring"#1F64FF"Pin color.kept
resolvedbooleanfalseWhether marked resolved.@deprecated
inProgressbooleanfalseWhether marked in-progress.@deprecated
lastUpdatedTimestamp (any){ seconds: 1717804800, nanoseconds: 0 }Last-updated time.auto-generated
createdAtTimestamp (any){ seconds: 1717800000, nanoseconds: 0 }Created time.auto-generated
positionXnumber320.5Pin X position.auto-generated
positionYnumber148.2Pin Y position.auto-generated
screenWidthnumber1440Author screen width.auto-generated
screenHeightnumber900Author screen height.auto-generated
screenScrollHeightnumber3200Author scroll height.auto-generated
screenScrollTopnumber640Author scroll-top offset.auto-generated
taggedElementPathstring"/html/body/div[1]/main/p[3]"XPath of the clicked element.auto-generated
taggedElementRectany (DOMRect-like){ x: 120, y: 300, width: 200, height: 24 }Bounding rect of the clicked element.auto-generated
targetElementTargetElement | null{ xpath: '/html/body/div', topPercentage: 42 }DOM anchor of the comment.kept — see TargetElement (its own targetText IS sent to Velt; only targetTextRange.text is withheld)
targetElementIdstring | null"editor-section-2"Target element id you supplied.kept
positionCursorPosition | null{ top: 148, left: 320 }Pin position descriptor.kept — see CursorPosition
locationIdnumber | null987654321Hash of location.kept
locationLocation | null{ id: 'page-1', version: { id: 'v1', name: 'V1' } }Sub-document location.kept — see Location
typestring"comment"Discriminator (comment | suggestion).default 'comment'; load-bearing suggestion discriminator
commentTypestring"suggestion"Secondary discriminator.load-bearing suggestion discriminator
sourceTypestring"agent"Origin; 'agent' = AI-authored.kept
metadataCommentMetadata (extends BaseMetadata; [key]: any){ documentId: 'doc_1', organizationId: 'org_1', apiKey: 'velt_xxx' }Org/doc/api routing context.full kept in Velt’s DB; getClientMetadata copy sent to your DB — see BaseMetadata
targetTextRangeTargetTextRange | null{ commonAncestorContainerFXpath: '/html/body/div/p', occurrence: 1 }Text-comment anchor.kept minus .text (only text is withheld) — see TargetTextRange
selectAllContentbooleantrueComment spans all text of the target element.kept
approvedbooleanfalseApproval flag.kept
statusCustomStatus{ id: 'OPEN', name: 'Open', type: 'DEFAULT', color: '#888' }Workflow status.default OPEN
statusUpdatedByUserIdstring | null"u_1"Who last changed status.already a userId
annotationIndexnumber11-based index in the available list.kept
pageInfoPageInfo{ title: 'Home', url: 'https://app.com' }Page context at creation.default new PageInfo() — see PageInfo
assignedToUser{ userId }{ userId: 'u_2' }Assignee.reduced when user provider active; copied-not-moved
priorityCustomPriority{ id: 'P1', name: 'High', color: '#f00' }Priority.kept
ghostCommentGhostComment | null { targetElement?; message?; type?; isSameGroup? }{ message: 'element not found', type: 'desktop' }Placeholder when target element is missing.kept
areaAnnotationIdstring"area_77"Connected area annotation id.kept
contextany{ access: { roles: ['editor'] } }Your custom context data.kept (unless in fieldsToRemove)
contextIdstring"ctx_abc123"Hash of context.kept
iamCommentIAMConfig { accessMode? }{ accessMode: 'PUBLIC' }IAM access config.default new CommentIAMConfig()
isPageAnnotationbooleanfalsePage-level annotation flag.kept
targetInlineCommentElementIdstring"inline-sec-3"Inline-comment target element id.kept
inlineCommentSectionConfigInlineCommentSectionConfig { id; name? }{ id: 'sec_3', name: 'Section 3' }Inline-comment section config.kept
customListCustomAnnotationDropdownItem[][{ id: 'tag1', label: 'Backend' }]Custom dropdown items.defaults []
subscribedUsersCommentAnnotationSubscribedUsers { [hash]: { user: User; type } }{ 'h_u1': { user: { userId: 'u_1' }, type: 'manual' } }Subscribed users.nested user reduced when user provider active
unsubscribedUsersCommentAnnotationUnsubscribedUsers{ 'h_u3': { user: { userId: 'u_3' }, type: 'auto' } }Unsubscribed users.nested user reduced when user provider active
subscribedGroupsCommentAnnotationSubscribedGroups { [groupId]: { type } }{ 'grp_eng': { type: 'manual' } }Subscribed groups.defaults {}
resolvedByUserIdstring | null"u_1"Who resolved the annotation.already a userId; copied-not-moved
multiThreadAnnotationIdstring"thread_5"Links multi-thread annotations.kept
isDraftbooleanfalseDraft flag.kept
sourceIdstring"src_99"Source id.kept
viewsCommentAnnotationViews{ views: { 'u_1': { timestamp: 1717800000 } } }View tracking.kept — see CommentAnnotationViews
viewedByUserIdsstring[]["u_1","u_2"]Viewer userIds.already userIds
suggestionSuggestionData{ status: 'pending', resolvedBy: { userId: 'u_1' } }SDK-managed suggestion data (iff type === 'suggestion').kept; nested resolvedBy reduced when user provider active
agentAgentData { agentName?; name?; avatar?; result?: { title? }; agentFields?[] }{ agentName: 'Reviewer Bot', result: { title: 'Fix typo' } }AI-agent identity + output.read-only from SDK; kept

1.B — Stored in your DB (PartialCommentAnnotation)

FieldTypeExampleDescriptionNotes
annotationIdstring"a8f3c2e1-…"Join key.required; also in Velt’s DB
metadataBaseMetadata{ documentId: 'doc_1', organizationId: 'org_1', apiKey: 'velt_xxx' }Client-facing metadata.getClientMetadata(data.metadata ?? {})
comments{ [commentId]: PartialComment }{ '482910': { commentId: 482910, commentText: 'Looks good', from: { userId: 'u_1' } } }Per-comment PII map.required ({}); re-keyed array → map
comments[].commentIdstring | number482910Per-comment id.always sent
comments[].commentHtmlstring"<p>Looks good 👍</p>"Rich-text body (PII).only if truthy; never sent to Velt (stripped on the frontend → your DB)
comments[].commentTextstring"Looks good 👍"Plain-text body (PII).only if truthy; never sent to Velt (stripped on the frontend → your DB)
comments[].attachments{ [attachmentId]: PartialAttachment }{ 1: { attachmentId: 1, name: 'spec.pdf', url: 'https://cdn/…/spec.pdf' } }Per-comment attachment name/url.only when the attachment resolver is also active
comments[].fromPartialUser { userId }{ userId: 'u_1' }Comment author.only if truthy
comments[].toPartialUser[][{ userId: 'u_2' }]@mentioned users.only if truthy
comments[].taggedUserContacts{ userId; contact?: { userId }; text? }[][{ userId: 'u_2', text: '@Jane' }]Tagged contacts (PII text labels).only if truthy
fromPartialUser { userId }{ userId: 'u_1' }Annotation creator.only if truthy; copied-not-moved
assignedToPartialUser { userId }{ userId: 'u_2' }Assignee.only if truthy; copied-not-moved
targetTextRange{ text: string }{ text: 'the selected sentence' }Selected text content (PII).the .text is never sent to Velt (stripped → your DB); rest of targetTextRange is sent
resolvedByUserIdstring | null"u_1"Who resolved it.only if truthy; copied-not-moved
[fieldsToRemove]any{ internalTicketId: 'JIRA-42' }Your custom fields kept off Velt entirely.per config.fieldsToRemove; truthy-gated; never sent to Velt (→ your DB)
[additionalFields]any{ teamName: 'Growth' }Your custom fields replicated.per config.additionalFields; deep-copied; sent to both (falsy preserved)

1.C — Comment strip rules

  • The only PII stripped on the frontend (and therefore never sent to Velt) is: per-comment commentText/commentHtml, attachment name/url (when the attachment resolver is active), targetTextRange.text, and any fieldsToRemove. Each comment whose PII is withheld gets isCommentResolverUsed = true; attachments get isAttachmentResolverUsed = true.
  • from / assignedTo / resolvedByUserId are copied-not-moved (sent to both).
  • A save to your provider only fires when the comment PII actually changed and the action maps to a ResolverActions value (COMMENT_ANNOTATION_ADD / COMMENT_ADD / COMMENT_UPDATE / COMMENT_DELETE, or a draft). Pure status / priority / assignment changes do not call save.
  • Truthy-gating: empty-string commentText etc. are not sent to your provider (and are not withheld from Velt either). additionalFields is the exception — it uses !== undefined, so 0 / "" / false are copied.
  • Delete sends only { apiKey, documentId, organizationId, folderId? }.

2. Reactions (reaction)

Model: ReactionAnnotation. PII payload: PartialReactionAnnotation.

2.A — Stored in Velt’s DB (ReactionAnnotation)

FieldTypeExampleDescriptionNotes
annotationIdstring"rXa92Kf0bQ3nLpT7v"Unique reaction-pin id.auto-generated; join key (also in your DB)
contextContext{ category: 'design' }Context object at creation.kept
commentAnnotationIdstring"cmtAnn_5fK28dQ"Connected comment annotation.kept
reactionsReaction[][{ variant: '1f44d', from: { userId: 'u_1' }, lastUpdated: <Date> }]Individual reactions.defaults []; each from reduced when user provider active — see Reaction sub-table
lastUpdatedany (server timestamp)1717804800000Annotation last-updated.auto-generated
targetElementTargetElement | null{ xpath: '#cta-btn' }DOM anchor.kept — see TargetElement
targetElementIdstring | null"cta-btn"Target element id you supplied.kept
positionCursorPosition | nullnullPin position.value never sent to Velt — written as null on every write to Velt’s DB (independent of self-hosting)
locationIdnumber | null1843762915Hash of location.auto-generated
locationLocation | null{ id: 'page-2', version: { id: 'v1', name: 'Draft' } }Sub-document location.kept — see Location
typestring"reaction"Discriminator.default 'reaction'
annotationIndexnumber31-based index in available list.kept
pageInfoPageInfo{ title: 'Dashboard', url: 'https://app.example.com/dashboard' }Page context.default new PageInfo() — see PageInfo
fromUser{ userId }{ userId: 'u_1' }Creator of the reaction annotation.reduced when user provider active; copied-not-moved
isReactionResolverUsedbooleantrueResolver-used flag.flag; set true on strip
metadataReactionMetadata (extends BaseMetadata; [key]: any){ documentId: 'doc_42', organizationId: 'org_7' }Routing metadata.full kept; getClientMetadata copy sent to your DB

2.B — Stored in your DB (PartialReactionAnnotation)

FieldTypeExampleDescriptionNotes
annotationIdstring"rXa92Kf0bQ3nLpT7v"Join key.required; also in Velt’s DB
metadataBaseMetadata{ documentId: 'doc_42', organizationId: 'org_7' }Client-facing metadata.getClientMetadata(annotation.metadata ?? {})
iconstring"1f44d"Emoji/icon code — the only relocated field.never sent to Velt
fromPartialUser { userId }{ userId: 'u_1' }Reaction creator.only if present; copied-not-moved

2.C — Reaction strip rules

  • Only icon is never sent to Velt (stripped on the frontend → your DB); everything else is kept and isReactionResolverUsed set true.
  • from is copied-not-moved. Per-element reactions[].from is reduced to { userId } (when user provider active) only inside Velt’s DB — it is not part of the Partial payload.
  • position’s value is never sent to Velt — written as null on every write to Velt’s DB regardless of self-hosting.
  • An unchanged save (deep-compare vs. cache) skips stripping entirely (icon not re-processed, flag not set).

3. Recordings (recorder)

Model: RecorderAnnotation. PII payload: PartialRecorderAnnotation.

3.A — Stored in Velt’s DB (RecorderAnnotation)

FieldTypeExampleDescriptionNotes
annotationIdstring"rec_a1b2c3d4"Unique recorder-pin id.auto-generated; join key
contextContext{ scopeId: 'scope-1' }Context object.kept
commentAnnotationIdstring"cmt_99x8"Connected comment annotation.kept
fromUser{ userId }{ userId: 'user-123' }Creator.reduced to { userId }; the full User (name/email/avatar) is never sent to Velt (→ your DB)
colorstring"#FF5733"Pin color.kept
lastUpdatedany1717804800000Last-updated time.auto-generated
positionXnumber342Pin X position.auto-generated
positionYnumber128Pin Y position.auto-generated
screenWidthnumber1920Author screen width.auto-generated
screenHeightnumber1080Author screen height.auto-generated
screenScrollHeightnumber4200Author scroll height.auto-generated
screenScrollTopnumber320Author scroll-top.auto-generated
recorderedElementPathstring"/html/body/div[2]/button[1]"XPath of clicked element.auto-generated
recorderedElementRectany{ top: 100, left: 50, width: 200, height: 40 }Bounding rect of clicked element.auto-generated
targetElementTargetElement | null{ xpath: '/html/body/div[2]' }DOM anchor.kept — see TargetElement
positionCursorPosition | null{ top: 128, left: 342 }Pin position.kept — see CursorPosition
locationIdnumber | null1837465921Hash of location.kept
locationLocation | null{ id: 'doc-1', locationName: 'Page 1' }Sub-document location.kept — see Location
typestring"recorder"Discriminator.default 'recorder'
recordingTypeRecorderType ('audio' | 'video' | 'screen')"video"Recording kind.default 'audio'
modeRecorderLayoutMode"floating"Recorder layout mode.default 'floating'
approvedbooleantrueApproval flag.kept
attachmentAttachment | nullnullSingle recorded-media attachment.@deprecated; value never sent to Velt — written as null; full object → your DB
attachmentsAttachment[][{ attachmentId: 'att_1', name: 'recording.webm', bucketPath: 'orgs/o1/rec/att_1.webm' }]Recording attachments.reduced to stubs { attachmentId, name, bucketPath } (url etc. never sent to Velt; bucketPath kept for cleanup)
annotationIndexnumber31-based index.kept
pageInfoPageInfo{ title: 'Home', url: 'https://app.example.com' }Page context.default new PageInfo() — see PageInfo
recordedTime{ duration?: number; display?: string } | null{ duration: 12500, display: '00:00:12' }Recorded duration.kept; sent to Velt
transcriptionTranscription(absent when resolver active)Transcript of the media.never sent to Velt when resolver active → see Your DB. Present in Velt’s DB only when no recorder resolver is set
waveformDatanumber[][0.1, 0.4, 0.2, 0.9]Audio waveform.kept; sent to Velt
displayNamestring"Sprint demo recording"Display name.kept; sent to Velt (top level)
metadataRecorderMetadata (extends BaseMetadata; [key]: any){ apiKey: 'AbC', documentId: 'doc-1', organizationId: 'org-1' }Routing metadata.full kept; getClientMetadata copy sent to your DB
latestVersionnumber2Current edit version number.kept
recordingEditVersions{ [version: number]: RecorderAnnotationEditVersion }{ 1: { recordedTime: {...}, waveformData: [...], displayName: 'v1' } }Per-edit version history.per-version PII withheld (from{userId}, attachmentnull, attachments→stubs, transcription never sent); non-PII per-version fields kept
chunkUrls{ [index: number]: string }{}Per-chunk upload URLs.value never sent to Velt — written as {} when resolver active (full map → your DB)
isRecorderResolverUsedbooleantrueResolver-used flag.flag; set true on strip
isUrlAvailablebooleanfalseReal URL (vs. local blob) ready yet.kept and copied to your DB

3.B — Stored in your DB (PartialRecorderAnnotation)

FieldTypeExampleDescriptionNotes
annotationIdstring"rec_a1b2c3d4"Join key.always present
metadataBaseMetadata{ apiKey: 'AbC', documentId: 'doc-1', organizationId: 'org-1', folderId: 'f-1' }Client-facing metadata.getClientMetadata(data.metadata)
fromUser (full){ userId: 'user-123', name: 'Jane Doe', email: 'jane@acme.com', photoUrl: 'https://…' }Full creator user object (PII).deep-cloned full object
transcriptionTranscription{ from: { userId: 'user-123' }, transcriptedText: 'Hello team…', vttUrl: 'https://…/cap.vtt' }Full transcript (PII).never sent to Velt — see Transcription
attachmentAttachment | null{ attachmentId: 'att_1', name: 'recording.webm', url: 'https://storage/…' }Deprecated single attachment (with URL).@deprecated; sent when defined; value never sent to Velt (written as null there)
attachmentsAttachment[][{ attachmentId: 'att_1', name: 'recording.webm', url: 'https://storage/…' }]Full attachment list incl. media URLs (PII).sent only when length > 0; reduced to stubs in Velt’s DB
chunkUrlsRecord<number, string>{ 0: 'https://storage/chunk0', 1: 'https://storage/chunk1' }Full chunk-URL map.sent only when non-empty; value never sent to Velt (written as {} there)
recordingEditVersionsRecord<number, PartialRecorderAnnotationEditVersion>{ 1: { from: { userId: 'user-123' }, attachments: [{ attachmentId: 'att_1', url: 'https://…' }], transcription: { transcriptedText: '…' } } }Per-version PII (from, attachment, attachments, transcription).only versions with ≥1 PII field
isUrlAvailablebooleanfalseReal URL ready yet.copied (also kept in Velt’s DB)
[additionalFields]any{ customTag: 'launch-demo' }Your replicated custom fields.per config.additionalFields; kept in Velt’s DB too

3.C — Recorder strip rules

  • Never sent to Velt: transcription (entire object → your DB), the value of attachment (written as null), the urls inside attachments (Velt keeps only { attachmentId, name, bucketPath } stubs; bucketPath is deliberately kept for Velt-side storage cleanup), and the value of chunkUrls (written as {}). from is reduced to { userId }; isRecorderResolverUsed set true.
  • recordingEditVersions per-version PII is withheld the same way; non-PII per-version fields (recordedTime, waveformData, displayName, boundedTrimRanges, boundedScaleRanges) are sent to Velt.
  • Top-level displayName / waveformData / recordedTime are sent to Velt (not part of the your-DB payload).
  • Recording files stay on Velt unless you also set recorder.storage (the bring-your-own storage scope). The recorder metadata resolver and the recording file storage are independent toggles.

4. Notifications (notification)

Model: Notification / NotificationRawData. PII payload: PartialNotification. Custom notifications only (notificationSource === 'custom'); standard comment/recording notifications resolve through their own feature providers. This is read-only enrichment — there is no write-side strip and no save.

4.A — Stored in Velt’s DB (structural notification envelope)

FieldTypeExampleDescriptionNotes
idstring"notif_a1b2c3d4"Notification id / join key.required; the resolver get() lookup key
notificationSourcestring"custom"Source discriminator.resolver applies only when 'custom'
actionTypestring"created"Action kind.structural
actionUserUser{ userId }{ userId: 'user_42' }Who triggered it.reduced when user provider active; full user object never sent to Velt
timestampnumber1717689600000Created time (epoch ms).ordering key
targetAnnotationIdstring"annotation_99"Comment annotation it points at.structural; underlying comment PII resolves via the comment resolver
metadataNotificationMetadata { apiKey?; organizationId?; clientOrganizationId?; documentId?; clientDocumentId?: string|number; locationId?: number; location?: Location }{ apiKey: 'key_xyz', organizationId: 'org_1', documentId: 'doc_5', locationId: 1234567890 }Routing identifiers.identifiers only (no display PII)
notifyUsers{ [emailHash]: boolean }{ "a1b2c3": true }Recipients by hashed email.keys are hashes, not raw emails
notifyUsersByUserId{ [userIdHash]: boolean }{ "f9e8d7": true }Recipients by hashed userId.keys are hashes
isCommentResolverUsedbooleantrueComment resolver supplied the comment text.flag
isNotificationResolverUsedbooleantrueNotification resolver enriched this record on read.flag; auto-set by merge

4.B — Stored in your DB (PartialNotification)

FieldTypeExampleDescriptionNotes
notificationIdstring"notif_a1b2c3d4"Join key.required; the only non-PII field
displayHeadlineMessageTemplatestring"{{actionUser}} mentioned {{recipientUser}}"Headline template (PII copy).never sent to Velt (custom)
displayHeadlineMessageTemplateData{ actionUser?: User; recipientUser?: User; actionMessage?: string; [key]: any }{ actionUser: { userId: 'user_42', name: 'Ada' }, actionMessage: 'mentioned you' }Headline template data (full user objects).PII; your DB only
displayBodyMessagestring"Take a look when you get a chance."Rendered body text.PII; your DB only
displayBodyMessageTemplatestring"{{users}} replied to your comment"Body template.PII; your DB only
displayBodyMessageTemplateData{ [key]: any }{ users: [{ userId: 'user_7', name: 'Linus' }] }Body template data.PII; your DB only
notificationSourceDataany{ comment: { text: 'Looks great!' } }Your custom source payload.PII; your DB only
[key: string]any{ customBadge: 'urgent' }Any extra custom fields you return.merged verbatim on read (except notificationId)

4.C — Notification strip rules

  • No write-side strip / no save. For custom notifications the PartialNotification PII is never sent to Velt at all; it lives in your backend and is fetched on read, then merged into both the notification and its raw form, setting isNotificationResolverUsed = true.
  • The only write-side reduction is actionUser → { userId } (when user provider active).
  • isUnread, forYou, and the rendered displayHeadlineMessage are computed on the client (from notificationViews / notifyUsers* / the templates) and stored in neither DB — omitted from the tables above.
  • Resolution order is notification → user → comment (notification PII fills userIds that the user resolver then enriches).
  • Delete calls your provider with { notificationId, organizationId }.

5. Activity (activity)

Model: ActivityRecord. PII payload: PartialActivityRecord. Append-only — there is no delete. Activity records can carry PII from several features at once.

5.A — Stored in Velt’s DB (ActivityRecord)

FieldTypeExampleDescriptionNotes
idstring"act_8f3kd92mz"Unique activity id / join key.auto-generated
featureTypeActivityFeatureType ('comment'|'reaction'|'recorder'|'crdt'|'custom')"comment"Feature that generated the activity.structural
actionTypestring"comment_annotation.status_change"Action (entity_type.action).structural
eventTypestring"STATUS_CHANGED"Underlying status/event.optional
actionUserUser{ userId }{ userId: 'user_123' }Who performed the action.reduced when user provider active; full user object never sent to Velt
timestampnumber1717804800000Server timestamp (epoch ms).auto-generated
metadataActivityMetadata (extends BaseMetadata; [key]: any){ organizationId: 'org_1', documentId: 'doc_42', folderId: 'folder_9' }Denormalized routing IDs.full kept; getClientMetadata copy sent to your DB
targetEntityIdstring"annotation_55"Parent entity id (annotation/recording/…).resolver lookup key
targetSubEntityIdstring | null"comment_88"Sub-entity id (commentId); null for entity-level.optional
changesActivityChanges { [key]: { from?; to? } }{ status: { from: { id: 'open' }, to: { id: 'closed' } } }Linear-style from/to pairs.user objects → { userId }; commentText entry never sent to Velt (→ your DB) when activity resolver active
entityDataTEntity (e.g. CommentAnnotation/ReactionAnnotation/RecorderAnnotation){ annotationId: 'annotation_55', status: { id: 'open' } }Full parent-entity snapshot.feature PII never sent to Velt (comment text/html/attachments, reaction icon, recorder transcription/attachments/chunkUrls); users → { userId }
entityTargetDataTTarget (e.g. Comment){ commentId: 'comment_88', from: { userId: 'user_123' } }Sub-entity snapshot (or custom-activity data).comment PII never sent to Velt (text/html/from/to/taggedUserContacts)
displayMessageTemplatestring"{{actionUser.name}} shipped {{releaseName}}"Template — custom activities only.kept (unless in fieldsToRemove)
displayMessageTemplateDataRecord<string, unknown>{ actionUser: { userId: 'user_123' }, releaseName: 'v2.1' }Template values — custom only.user objects → { userId }; non-user values kept
actionIconstring"https://cdn.example.com/icons/status.svg"Display icon (typically custom).optional
immutablebooleantrueIf true, cannot be updated/deleted via REST.optional flag
isActivityResolverUsedbooleantrueResolver withheld PII from this record (PII never sent to Velt).flag; set true only when PII was extracted

5.B — Stored in your DB (PartialActivityRecord)

FieldTypeExampleDescriptionNotes
idstring"act_8f3kd92mz"Correlation key.required; same as ActivityRecord.id
metadataBaseMetadata{ organizationId: 'org_1', documentId: 'doc_42' }Client-facing metadata copy.getClientMetadata subset
changesActivityChanges{ commentText: { from: 'old text', to: 'new text' } }PII change pairs.for comment activities, only { commentText }; sent when activity resolver active
entityDataunknown (PartialReaction…/PartialRecorder…){ annotationId: 'rec_77', transcription: {…}, attachment: {…} }Feature-specific entity PII.reaction/recorder only, and only when the matching feature resolver is also active
entityTargetDataunknown{ commentId: 'comment_88', commentText: 'Looks good' }Sub-entity PII snapshot.merged back on read
displayMessageTemplateDataRecord<string, unknown>{ releaseName: 'v2.1', secretNote: 'internal only' }Custom-activity template values.via fieldsToRemove for custom; users → { userId } Velt-side
[key: string]any{ customField: { ssn: '***' } }fieldsToRemove custom fields.featureType === 'custom' only; never sent to Velt (→ your DB)

5.C — Activity strip rules

  • displayMessage is always recomputed on the client (from the template + values) and stored in neither DB — omitted from the tables above.
  • All user reduction (actionUser, users in changes, users in displayMessageTemplateData) happens only when the user provider is active.
  • changes['commentText'] is never sent to Velt (→ your DB) only when the activity resolver is active; if only the comment resolver is active it is preserved (to avoid unrestorable loss). Comment entityData/entityTargetData PII is handled by the comment resolver’s own store.
  • Reaction/recorder entityData PII reaches your DB only when both the activity resolver and the matching feature resolver are active.
  • fieldsToRemove applies to featureType === 'custom' only; built-in feature types ignore it.
  • Append-only: no delete.

6. Attachments (attachment)

Stored model: Attachment — embedded on comments (comments[].attachments) and recordings (attachments). Upload payload: ResolverAttachment / AttachmentResolverMetadata. There is no Partial<X> strip and no get — attachments are binary files. When you register an attachment (or recorder.storage) provider, Velt hands you the raw File; you return { url }.

6.A — Stored in Velt’s DB (Attachment record — kept, minus the binary bytes)

FieldTypeExampleDescriptionNotes
attachmentIdnumber742193Unique attachment id (random 6-digit).auto-generated; primary key
isAttachmentResolverUsedbooleantrueSelf-hosted storage used for this attachment.flag
namestring"design-spec.pdf"Original file name.kept; also sent (reduced) to your storage
bucketPathstring"attachments/abc123/design-spec.pdf"Path of the file in storage.kept
sizenumber204800File size (bytes).kept
typeRecorderFileFormat ('mp3'|'mp4'|'webm')"mp4"File format.kept (string union, not enum)
urlstring"https://customer-cdn.example.com/files/design-spec.pdf"Download URL.with self-hosting = the URL your storage returned
thumbnailstring"data:image/png;base64,iVBORw0KGgo…"Base64 thumbnail.kept
thumbnailWithPlayIconUrlstring"https://cdn/…/thumb-play.png"Thumbnail with play overlay (video).kept
metadataany{ width: 1920, height: 1080, duration: 12.4 }Arbitrary attachment metadata.kept (distinct from AttachmentResolverMetadata)
mimeTypeany"application/pdf"MIME type.kept; also sent (reduced) to your storage
previewImagesstring[]["data:image/png;base64,…","data:image/png;base64,…"]Preview images.kept
The binary bytes are never sent to Velt when you self-host storage; they go straight to your bucket and only the returned url (plus the structural fields above) is stored on the Attachment record.

6.B — Handed to your storage provider (upload payload) / returned

FieldTypeExampleDescriptionNotes
fileFileFile { name: 'mockup.png', size: 23456, type: 'image/png' }Raw binary content.multipart file part (URL mode) or provider.save arg; never sent to Velt
attachment.attachmentIdnumber742199Attachment id (inside request.attachment).required
attachment.namestring"mockup.png"File name.optional
attachment.mimeTypestring"image/png"MIME type.optional
metadataAttachmentResolverMetadata{ organizationId: 'org_xyz789', documentId: 'doc_a1b2c3', folderId: null, attachmentId: 742199, commentAnnotationId: 'ann_55', apiKey: 'AbCd…' }Routing context.see sub-fields below
metadata.organizationIdstring | null"org_xyz789"Org scope.nullable
metadata.documentIdstring | null"doc_a1b2c3"Document scope.nullable
metadata.folderIdstring | null"folder_q1reports"Folder scope.optional + nullable; on delete only when truthy
metadata.attachmentIdnumber | null742199Mirror of top-level id.nullable
metadata.commentAnnotationIdstring | null"ann_55"Owning comment annotation.nullable; dropped on delete
metadata.apiKeystring | null"AbCdEf123456PublicApiKey"Velt public API key.nullable
eventResolverActions"ATTACHMENT_SAVE"Why the call fired.optional; delete uses ATTACHMENT_DELETE
(return) urlstring"https://cdn.customer.com/velt/742199/mockup.png"URL of the stored file you return.SaveAttachmentResolverData.url; persisted back onto Attachment.url

6.C — Attachment strip rules

  • No annotation-style Partial<X>. The JSON request is exactly { attachment: { attachmentId, name, mimeType }, metadata, event }; the File is destructured out and sent as binary — straight to your storage, never to Velt.
  • Delete sends { attachmentId, metadata: { apiKey, documentId, organizationId, folderId? }, event }.
  • Comment attachments (DataProviders.attachment) and recording files (DataProviders.recorder.storage) are independent storage scopes — omit either to keep that scope on Velt-managed storage.

7. Shared building blocks

These nested objects are embedded inside the feature records above and are kept verbatim in Velt’s DB (unless a sub-field is noted as never sent to Velt). The metadata envelope is the one whose your-DB copy is reduced via getClientMetadata().

BaseMetadata

The metadata field present on every feature payload. Full version kept in Velt’s DB; client-facing subset sent to your DB.
FieldTypeExampleDescriptionNotes
apiKeystring"AbCdEf123456PublicApiKey"Velt public API key.optional
documentIdstring"doc_a1b2c3"Velt-internal hashed document id.auto-derived from clientDocumentId
clientDocumentIdstring"my-page-123"The raw documentId you passed.dropped from the client-facing copy (after mapping → documentId)
organizationIdstring"org_xyz789"Velt-internal hashed org id.auto-derived from clientOrganizationId
clientOrganizationIdstring"acme-corp"The raw organizationId you passed.dropped from the client-facing copy (after mapping → organizationId)
folderIdstring"folder_q1reports"Velt-internal hashed folder id.auto-derived from veltFolderId
veltFolderIdstring"vfolder_001"Velt-assigned folder id.Velt-internal; not included in the client-facing copy sent to your DB
documentMetadataDocumentMetadata{ documentName: 'Q1 Report' }Your descriptive document metadata.optional
sdkVersionstring | null"5.0.2-beta.34"SDK version that produced the payload.auto-generated
Client-facing transform (getClientMetadata): clientDocumentId → documentId, clientOrganizationId → organizationId; veltFolderId / parentVeltFolderId / pageInfo dropped. folderId included only when truthy.

Location & Version

FieldTypeExampleDescriptionNotes
idstring"page-2"Unique location id.optional
locationNamestring"Page 2 - Section A"Human-readable location name.optional
versionVersion { id: string; name: string }{ id: 'v1', name: 'Draft 1' }Version object.optional
[key: string]any{ page: 3, tab: 'design' }Arbitrary custom location keys.open index signature — kept

PageInfo

FieldTypeExampleDescriptionNotes
urlstring"https://app.example.com/editor?doc=123"Full page URL.optional
pathstring"/editor"Path (excl. base URL).optional
queryParamsstring"doc=123&tab=design"Query string.optional
baseUrlstring"https://app.example.com"Domain.optional
titlestring"Editor - Acme App"Page title.optional
arrowUrlstring"https://app.example.com/editor#arrow-1"Arrow-annotation reference URL.optional
areaUrlstring"https://app.example.com/editor#area-1"Area-annotation reference URL.optional
commentUrlstring"https://app.example.com/editor#comment-1"Comment-annotation reference URL.optional
tagUrlstring"https://app.example.com/editor#tag-1"Tag-annotation reference URL.optional
recorderUrlstring"https://app.example.com/editor#rec-1"Recorder-annotation reference URL.optional
screenWidthnumber1440Author screen width.auto-generated
deviceInfoIDeviceInfo{ os: 'macOS', browser: 'Chrome' }Device/browser info.auto-generated default

TargetElement

FieldTypeExampleDescriptionNotes
xpathstring"/html/body/div[1]/button"XPath of the target element.optional
fXpathstring"/html[1]/body[1]/div[1]/button[1]"Full XPath.optional
cfXpathstring"/html[1]/body[1]/div[1]@class=toolbar/button[1]"Full XPath with class names.optional
topPercentagenumber42.5Relative top position on the element (%).default 0
leftPercentagenumber63.1Relative left position on the element (%).default 0
anchorAnchorRecord | null{ … robust anchor descriptor … }Robust anchor descriptor.optional

TargetTextRange

FieldTypeExampleDescriptionNotes
commonAncestorContainerstring"/html/body/div[2]/p[1]"XPath of the common ancestor container.optional
commonAncestorContainerFXpathstring"/html[1]/body[1]/div[2]/p[1]"Full XPath of the container.optional
commonAncestorContainerCFXpathstring"/html[1]/body[1]/div[2]@class=content/p[1]"Full XPath with class names.optional
commonAncestorContainerAnchorAnchorRecord{ … robust anchor descriptor … }Robust anchor descriptor.optional
textstring"the quick brown fox"The selected text snippet.never sent to Velt for comments (→ your DB) — the only sub-field withheld
occurrencenumber1Which occurrence of the text.default 1

CursorPosition

FieldTypeExampleDescriptionNotes
topnumber148Top position of the pin/cursor.default 0
leftnumber320Left position of the pin/cursor.default 0
parentScaleXnumber1X-scale of the parent element.optional (transform handling)
parentScaleYnumber1Y-scale of the parent element.optional (transform handling)
transformContextany{ … }Transform context info.optional

CommentAnnotationViews

FieldTypeExampleDescriptionNotes
views{ [userId]: { timestamp } }{ 'u_1': { timestamp: 1717800000 } }Per-user annotation view timestamps.kept
comments{ [commentId]: { views: { [userId]: { timestamp } } } }{ '482910': { views: { 'u_1': { timestamp: 1717800100 } } } }Per-comment view timestamps.kept
metadataBaseMetadata{ documentId: 'doc_1' }Routing metadata.optional

Reaction (per element)

The element shape of ReactionAnnotation.reactions[], kept in Velt’s DB.
FieldTypeExampleDescriptionNotes
variantstring"1f44d"Emoji variant/identifier for this reaction.optional; kept; sent to Velt
fromUser{ userId }{ userId: 'u_1' }Who added this individual reaction.required on element; reduced when user provider active (Velt-side only)
lastUpdatedDate2026-06-08T10:32:00.000ZWhen this reaction was added.auto-generated

Comment (per-thread comment)

The element shape of CommentAnnotation.comments[], kept in Velt’s DB with per-comment PII never sent to Velt.
FieldTypeExampleDescriptionNotes
commentIdnumber482910Per-comment id.auto-generated
contextany{ … }Per-comment context.kept
type'text' | 'voice'"text"Comment content type.default 'text'
commentTextstring"Looks good 👍"Plain-text body.never sent to Velt (→ your DB)
isCommentTextAvailablebooleantrueWhether comment text exists.kept
isCommentResolverUsedbooleantrueComment resolver used (per comment).flag
commentHtmlstring"<p>Looks good 👍</p>"Rich-text body.never sent to Velt (→ your DB)
replaceContentHtmlstring"<p>fixed</p>"HTML to replace on accept.kept
replaceContentTextstring"fixed"Text to replace on accept.kept
commentVoiceUrlstring"https://…/voice.webm"Voice-comment URL.kept
fromUser{ userId }{ userId: 'u_1' }Comment author.reduced when user provider active
toUser[]{ userId }[][{ userId: 'u_2' }]@mentioned users.reduced when user provider active
lastUpdatedDate2026-06-08T10:32:00ZLast-updated time.auto-generated
createdAtany{ seconds: 1717800000 }Created time.auto-generated
status'added' | 'updated'"added"Comment status.default 'added'
attachmentsAttachment[][{ attachmentId: 1, name: 'spec.pdf' }]Attachments.name/url never sent to Velt when attachment resolver active — see Attachment
recordersRecordedData[][…]Embedded recorded data.kept
reactionAnnotationIdsstring[]["rXa92…"]Linked reaction-annotation ids.kept
taggedUserContactsAutocompleteUserContactReplaceData[][{ userId: 'u_2', text: '@Jane' }]Tagged contacts.contact reduced to { userId }; PII text → your DB
customListAutocompleteReplaceData[][…]Custom autocomplete replacements.kept
toOrganizationUserGroupAutocompleteGroupReplaceData[][…]Tagged org groups.kept
isDraftbooleanfalseDraft flag.kept
editedAtany{ seconds: 1717800500 }Edited time.auto-generated
isEditedbooleantrueEdited flag.kept

Transcription (your-DB recorder payload)

Full object sent to your DB (never sent to Velt) when the recorder resolver is active.
FieldTypeExampleDescriptionNotes
fromUser{ userId: 'user-123' }Who created the transcription.required
lastUpdatednumber1717804800000Created/updated time.optional
srtBucketPathstring"orgs/o1/rec/cap.srt"Storage path of the SRT file.optional
srtUrlstring"https://…/cap.srt"SRT file URL.optional
transcriptedTextstring"Hello team, in this demo…"Transcribed text.optional (PII)
transcriptionLatencynumber840Time to transcribe (ms).optional
vttBucketPathstring"orgs/o1/rec/cap.vtt"Storage path of the VTT file.optional
vttUrlstring"https://…/cap.vtt"VTT file URL.optional

8. Summary matrix & field counts

Where does each field-class live?

DataVelt’s DBYour DB / storage
Comment text / HTML
Comment attachment file + name/urlbucketPath stub (recordings)
Reaction icon
Recording transcript / filefile stubs only
Custom-notification display/template PII
Activity PII (commentText changes, entity data, template data)
User profile (name, email, avatar){ userId } only (when user provider set)
Your custom fields via fieldsToRemove
Your custom fields via additionalFields
Structural data (IDs, locations, status, targets, timestamps, flags)
Client-computed fields (unread, isUnread, forYou, displayMessage, …)
Anything in the right-hand column with under “Velt’s DB” is never sent to Velt — it is stripped on the frontend and goes only to your DB (or is recomputed on the client).

Field counts (persisted fields only)

FeatureVelt’s DBYour DB (Partial<X>)Strip model
Comments53 top-level (+ ~24-field Comment element)PartialCommentAnnotation + nested PartialCommentstrip on the frontend, merge on read
Reactions16 top-level (+ 3-field Reaction element){ annotationId, metadata, icon, from? }strip icon only
Recordings36 (incl. reduced from/attachments/chunkUrls; transcription → your DB only)PartialRecorderAnnotation (+ per-version partials, additionalFields)strip + reduce on the frontend; optional file storage
Notifications11 (structural + flags)PartialNotification (6 PII + open index)read-only enrichment (custom only)
Activity17PartialActivityRecord ({ id, metadata?, changes?, entityData?, entityTargetData?, displayMessageTemplateData?, [key]: any })strip on the frontend; append-only
AttachmentsAttachment (12 fields, minus bytes)file + { attachmentId, name, mimeType } + AttachmentResolverMetadata + event → returns { url }binary file storage only

Resolver flags (set in Velt’s DB; never sent from your side)

isCommentResolverUsed · isReactionResolverUsed · isRecorderResolverUsed · isNotificationResolverUsed · isActivityResolverUsed · isAttachmentResolverUsed