Skip to content
bitzorcas
中EN

Guide

Documents Whiteboards and Real-Time Collaboration

Whiteboard versions and members, in-memory text operations, logging-only hubs, multi-instance risks, and a production collaboration architecture.

Last updated

Documents contains both a persistent whiteboard aggregate and a simplified text-collaboration service. The whiteboard can replace canvas JSON with an integer version check. The collaboration service keeps text and participants inside one process and sends notifications to a logging-only default hub. They are not one real-time collaboration protocol.

1. Whiteboard model

Whiteboard stores Name, Description, Type, optional KnowledgeBaseId, Active/Archived state, Data JSON text, CurrentVersion, AllowCollaboration, DefaultAccessLevel, and embedded members.

create, Data={}, Version=1UpdateData(expected=current) / version +1archiveUpdateData rejected

Active

Archived

UpdateData replaces the complete Data string and increments the version only when the request version equals CurrentVersion. It does not validate JSON syntax/schema or size, and the aggregate has no database condition token by itself.

2. HTTP use cases

Method and routeBehaviorCurrent security boundary
POST /api/v1/whiteboardsCreate Active boarddocs.whiteboard.create; no KB/member enforcement
GET /api/v1/whiteboards/{id}Full Data detaildocs.whiteboard.view, via IAuthorizedRequest
GET /api/v1/whiteboardsFiltered listdocs.whiteboard.view, via IAuthorizedRequest
PUT .../{id}/dataFull replace with version checkdocs.whiteboard.update
POST/DELETE .../{id}/membersMutate member JSONdocs.whiteboard.update; members not later enforced
POST .../{id}/archiveActive → Archiveddocs.whiteboard.update
DELETE .../{id}Soft deletedocs.whiteboard.delete

The permission catalog now defines four whiteboard permission codes: docs.whiteboard.view, docs.whiteboard.create, docs.whiteboard.update, and docs.whiteboard.delete (resource identifier whiteboard). All whiteboard commands and queries implement IAuthorizedRequest with an explicit ResourceDescriptor(DocumentsPermissions.Module, DocsPermissions.WhiteboardResource) and a concrete AuthorizationAction. Member relationships still do not participate in later object-level authorization; that is the next gap to close.

3. Version-aware canvas save

Handle a whiteboard version conflict
type Whiteboard = {
whiteboardId: string;
data: string;
currentVersion: string; // DTO is string; the aggregate stores int.
};
export async function saveCanvas(
board: Whiteboard,
canvas: unknown,
): Promise<Whiteboard> {
const response = await fetch(
`/api/v1/whiteboards/${board.whiteboardId}/data`,
{
method: "PUT",
credentials: "include",
headers: { "content-type": "application/json" },
body: JSON.stringify({
// The current server does not validate this JSON schema.
data: JSON.stringify(canvas),
version: Number(board.currentVersion),
}),
},
);
if (response.status === 409)
throw new Error("canvas changed; reload and merge before retrying");
// Do not retry a full snapshot blindly after a version conflict.
if (!response.ok) throw new Error(`save failed: ${response.status}`);
return response.json() as Promise<Whiteboard>;
}

The response string/request int mismatch should be corrected to avoid cross-language conversion errors. A database conditional update must ensure two equal expected versions cannot both commit.

4. Collaboration-session aggregate

The persistent aggregate contains TenantId, DocumentId, Active/Inactive, CreatorId, EndedAt, and ParticipantsJson. It rejects duplicate active participants, marks leave records inactive, permits a user to rejoin as a new record, and ends idempotently.

SimpleCollaborationService does not save that aggregate. It keeps another CollaborationSession and text state in ConcurrentDictionary<string, State> keyed by DocumentId.

5. Actual service flow

NullRealtimeCollaborationHubConcurrentDictionarySimpleCollaborationServiceUserNullRealtimeCollaborationHubConcurrentDictionarySimpleCollaborationServiceUserJoin(documentId, userId, displayName)GetOrAdd(documentId)create session with "default-tenant"join participantfire-and-forget joined notificationApplyOperationmutate in-memory textappend operation logfire-and-forget operation and full content

Important constraints:

  • tenant is hard-coded to default-tenant;
  • state is partitioned only by DocumentId, not tenant/session/node;
  • initial content is empty and never loads Document.Content;
  • edits are not saved to Document or the session table;
  • restart, scale-out, or node switching loses/splits state;
  • hub calls are fire-and-forget and exceptions are unobserved;
  • the default hub only logs; it sends no SignalR/WebSocket message;
  • no HTTP or hub endpoint currently calls the collaboration service.

6. Operations are neither OT nor CRDT

Insert, Delete, and Replace are applied to the current string in arrival order after checking active participation and position.

Single-process collaboration operation
// The current interface accepts userId directly; a production gateway derives it from auth.
var join = await collaboration.JoinSessionAsync(
"doc-42", "user-a", "Alice", cancellationToken);
if (join.IsFailure) return join.Error;
// OperationId is not a deduplication key in the current in-memory service.
var operation = new CollaborationOperation(
OperationId: Guid.NewGuid().ToString("N"),
UserId: "user-a",
Type: OperationType.Insert,
Position: 0,
Length: 0,
Content: "Hello");
// OperationId is checked for non-empty but is not deduplicated.
var applied = await collaboration.ApplyOperationAsync(
"doc-42", operation, cancellationToken);

Although a comment says LWW, there is no timestamp or logical-clock comparison. Concurrent positions are not transformed. An overlong Delete removes through the end; an overlong Replace inserts without first deleting; an unknown operation type returns success with unchanged content.

7. Whiteboard and collaboration are disconnected

IWhiteboardCollaborationHub also defaults to logging only. UpdateWhiteboardData does not call it. SimpleCollaborationService edits its own text, not Whiteboard.Data. Consequently, AllowCollaboration does not open a channel, whiteboard updates do not broadcast, member revocation does not terminate connections, edits do not create Document versions, and the database session cannot restore in-memory collaboration state.

8. Production real-time architecture

Web / desktop client

Authenticated real-time gateway

DocumentAccessPolicy

session coordinator
Tenant + Document partition

OT / CRDT engine

operation log + snapshot

backplane / fan-out

explicit Document version commit

Specify session identity, algorithm, OperationId idempotency, snapshots and replay, access revocation, version-commit timing, multi-node ownership, limits, and content retention before implementation.

9. Security blockers

FindSessionByIdAsync and FindActiveSessionByDocumentAsync have no TenantId argument or predicate. Any future caller must fix the interface before exposure.

SimpleCollaborationService does not verify Document existence, AllowCollaboration, caller identity, or resource access. Its direct userId parameter enables impersonation if exposed. A gateway must derive the subject from authenticated context.

10. Test matrix

LayerRequired coverage
Whiteboard aggregatemismatch, archive, invalid/large Data, duplicate members, DTO type
Database concurrencytwo equal expected versions, exactly one commit
Collaboration algorithmduplicate/ordered operations, concurrent inserts, bounds, unknown type
Multitenancysame DocumentId in two tenants; session-store predicates
Real-time securityforged user, ACL revocation, token expiry, reconnect and limits
Scale-outnode change, restart recovery, backplane replay, split ownership
Persistenceoperation log, snapshots, and Document-version commit compensation

Back to Documents

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%