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.
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 route | Behavior | Current security boundary |
|---|---|---|
POST /api/v1/whiteboards | Create Active board | docs.whiteboard.create; no KB/member enforcement |
GET /api/v1/whiteboards/{id} | Full Data detail | docs.whiteboard.view, via IAuthorizedRequest |
GET /api/v1/whiteboards | Filtered list | docs.whiteboard.view, via IAuthorizedRequest |
PUT .../{id}/data | Full replace with version check | docs.whiteboard.update |
POST/DELETE .../{id}/members | Mutate member JSON | docs.whiteboard.update; members not later enforced |
POST .../{id}/archive | Active → Archived | docs.whiteboard.update |
DELETE .../{id} | Soft delete | docs.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
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
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.
// 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
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
| Layer | Required coverage |
|---|---|
| Whiteboard aggregate | mismatch, archive, invalid/large Data, duplicate members, DTO type |
| Database concurrency | two equal expected versions, exactly one commit |
| Collaboration algorithm | duplicate/ordered operations, concurrent inserts, bounds, unknown type |
| Multitenancy | same DocumentId in two tenants; session-store predicates |
| Real-time security | forged user, ACL revocation, token expiry, reconnect and limits |
| Scale-out | node change, restart recovery, backplane replay, split ownership |
| Persistence | operation log, snapshots, and Document-version commit compensation |