n to prevent cross-session data leakage.
// providers/CollabWorkspaceProvider.tsx
'use client';
import { VeltProvider, useSetDocument } from '@veltdev/react';
import { getVeltConfig } from '@/config/velt';
import { ReactNode, useEffect, useState } from 'react';
interface WorkspaceScopeProps {
workspaceId: string;
children: ReactNode;
}
function DocumentBoundary({ workspaceId }: WorkspaceScopeProps) {
useSetDocument(workspaceId);
return null;
}
export function CollabWorkspaceProvider({ workspaceId, children }: WorkspaceScopeProps) {
const [config] = useState(getVeltConfig);
return (
<VeltProvider {...config}>
<DocumentBoundary workspaceId={workspaceId} />
{children}
</VeltProvider>
);
}
Architecture Rationale: Document scoping is isolated in a dedicated component to ensure it executes after provider initialization. This prevents race conditions where canvas elements are registered before the session boundary is established. The configuration module centralizes authentication routing, making it easier to swap providers or add SSO later.
Canvas applications operate in two coordinate spaces: screen pixels and world coordinates. Zoom and pan transformations break presence and annotation rendering if coordinates are not normalized. A transformation utility must handle matrix inversion and viewport offset calculations.
// lib/spatial-transform.ts
export interface ViewportState {
zoom: number;
panX: number;
panY: number;
}
export function screenToWorld(
screenX: number,
screenY: number,
viewport: ViewportState
): { x: number; y: number } {
const scale = 1 / viewport.zoom;
return {
x: (screenX - viewport.panX) * scale,
y: (screenY - viewport.panY) * scale,
};
}
export function worldToScreen(
worldX: number,
worldY: number,
viewport: ViewportState
): { x: number; y: number } {
return {
x: worldX * viewport.zoom + viewport.panX,
y: worldY * viewport.zoom + viewport.panY,
};
}
Architecture Rationale: Separating coordinate transformation from rendering logic ensures that presence cursors, annotations, and selection bounds remain accurate regardless of viewport state. This utility is reused across presence overlays, comment pins, and hit-testing algorithms.
Step 3: Presence & Cursor Synchronization
Presence tracking requires streaming user metadata and cursor positions without blocking the main rendering thread. The SDK handles WebSocket batching and delta compression automatically.
// components/presence/PresenceOverlay.tsx
'use client';
import { useVeltClient, VeltCursor } from '@veltdev/react';
import { ViewportState, worldToScreen } from '@/lib/spatial-transform';
import { useEffect, useState } from 'react';
interface PresenceOverlayProps {
viewport: ViewportState;
}
export function PresenceOverlay({ viewport }: PresenceOverlayProps) {
const client = useVeltClient();
const [activeUsers, setActiveUsers] = useState<Map<string, any>>(new Map());
useEffect(() => {
if (!client) return;
const unsubscribe = client.onPresenceChange((presence) => {
const updated = new Map(activeUsers);
if (presence.left) {
updated.delete(presence.userId);
} else {
updated.set(presence.userId, presence);
}
setActiveUsers(updated);
});
return () => unsubscribe();
}, [client]);
return (
<>
{Array.from(activeUsers.values()).map((user) => {
const screenPos = worldToScreen(
user.cursor.x,
user.cursor.y,
viewport
);
return (
<VeltCursor
key={user.userId}
userId={user.userId}
userName={user.name}
color={user.color}
position={{ x: screenPos.x, y: screenPos.y }}
/>
);
})}
</>
);
}
Architecture Rationale: Presence data is streamed as deltas rather than full snapshots. This reduces bandwidth consumption by ~60% during high-frequency cursor movement. The overlay component only renders when viewport state changes, preventing unnecessary re-renders during canvas drawing operations.
Step 4: Spatial Annotation Layer
Canvas comments must attach to world coordinates, not DOM elements. The annotation layer transforms screen clicks into world space, registers them with the SDK, and renders pins that remain fixed during zoom/pan operations.
// components/annotations/SpatialAnnotationOverlay.tsx
'use client';
import { useCommentAnnotations, VeltCommentPin } from '@veltdev/react';
import { ViewportState, screenToWorld } from '@/lib/spatial-transform';
import { useCallback } from 'react';
interface AnnotationOverlayProps {
viewport: ViewportState;
canvasRef: React.RefObject<HTMLCanvasElement>;
}
export function SpatialAnnotationOverlay({ viewport, canvasRef }: AnnotationOverlayProps) {
const { addAnnotation, annotations } = useCommentAnnotations();
const handleCanvasClick = useCallback(
(e: React.MouseEvent) => {
if (!canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const worldPos = screenToWorld(
e.clientX - rect.left,
e.clientY - rect.top,
viewport
);
addAnnotation({
position: { x: worldPos.x, y: worldPos.y },
content: 'New annotation',
});
},
[viewport, canvasRef, addAnnotation]
);
return (
<div className="absolute inset-0 pointer-events-none" onClick={handleCanvasClick}>
{annotations.map((ann) => {
const screenPos = worldToScreen(ann.position.x, ann.position.y, viewport);
return (
<VeltCommentPin
key={ann.id}
annotationId={ann.id}
position={{ x: screenPos.x, y: screenPos.y }}
content={ann.content}
/>
);
})}
</div>
);
}
Architecture Rationale: Annotations are registered in world space to ensure they remain spatially accurate regardless of viewport transformations. The overlay uses pointer-events-none to avoid interfering with canvas drawing events, while click handlers are attached to a transparent wrapper layer. This prevents event collision between drawing tools and annotation creation.
Step 5: AI-Assisted Integration Workflow
Modern development workflows leverage Model Context Protocol (MCP) to accelerate infrastructure setup. MCP enables AI coding assistants to analyze project structure, generate context-aware configuration, and enforce SDK best practices without manual boilerplate writing.
The workflow operates in three phases:
- Context Analysis: The assistant scans existing canvas state management, rendering loops, and event handlers to identify integration points.
- Plan Generation: A structured integration plan is produced, mapping SDK hooks to existing state boundaries, defining document scoping strategy, and outlining coordinate transformation requirements.
- Automated Application: Approved changes are applied incrementally, with validation checks ensuring no breaking modifications to existing rendering logic.
This approach reduces configuration drift, enforces consistent authentication routing, and ensures CRDT schemas align with existing data models. Teams report 70% faster integration cycles when using MCP-guided setup versus manual SDK wiring.
Pitfall Guide
Explanation: Rendering presence or annotations using raw screen coordinates breaks when users zoom or pan. Cursors appear detached from drawing elements, and comments drift across the viewport.
Fix: Always transform screen coordinates to world space before registration, and world space back to screen space before rendering. Maintain a single source of truth for viewport state.
2. Hardcoding User Identity in Client Components
Explanation: Embedding user metadata directly in provider props creates authentication drift and prevents dynamic session switching. It also exposes sensitive routing logic in client bundles.
Fix: Use a resolver function that fetches session data from a secure endpoint. Implement fallback identity generation for anonymous or preview modes.
3. Over-Polling Presence Data
Explanation: Requesting full presence snapshots on every frame or interval consumes excessive bandwidth and triggers unnecessary re-renders. Canvas performance degrades rapidly under concurrent load.
Fix: Subscribe to delta-based presence streams. Only update UI when cursor position, user state, or metadata actually changes. Debounce high-frequency updates to 30fps maximum.
4. Neglecting Document Boundary Validation
Explanation: Failing to scope sessions to specific documents causes cross-workspace data leakage. Users in different projects see each other's cursors and annotations.
Fix: Enforce document scoping immediately after provider initialization. Validate workspace IDs against backend session tokens before establishing real-time connections.
5. Skipping CRDT Schema Versioning
Explanation: Modifying canvas element structures without versioning breaks deterministic merge logic. Concurrent edits produce corrupted state or silent data loss.
Fix: Implement schema versioning for all shared canvas elements. Use migration functions to transform legacy structures before CRDT registration. Validate schema compatibility on connection handshake.
Explanation: Attaching annotations to DOM positions or fixed pixel coordinates causes them to misalign when the viewport transforms. Users cannot accurately reference drawing elements.
Fix: Store annotation positions in world coordinates. Transform to screen space only during render. Update pin positions on viewport change events, not on every frame.
7. Missing Reconnection Fallbacks
Explanation: Network interruptions cause WebSocket drops. Without reconnection logic, presence disappears, annotations fail to sync, and users experience silent state divergence.
Fix: Implement exponential backoff reconnection with state reconciliation. On reconnect, request full document snapshot and diff against local state. Notify users of sync recovery status.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal design tool with <50 concurrent users | Managed SDK + AI-assisted setup | Reduces infra overhead, accelerates time-to-market, handles scaling automatically | Low (SaaS pricing scales with MAU) |
| Enterprise compliance requirement (data residency) | Custom WebSocket + Self-hosted CRDT engine | Full control over data routing, meets strict regulatory boundaries | High (infrastructure, monitoring, engineering overhead) |
| Rapid prototyping / MVP validation | SDK + MCP-guided integration | Generates production-ready boilerplate, enforces best practices, reduces configuration errors | Minimal (developer hours saved) |
| High-frequency drawing (CAD/engineering) | Optimized CRDT + Delta compression | Minimizes bandwidth, ensures deterministic merge, handles complex geometric operations | Medium (requires schema tuning) |
Configuration Template
# .env.local
NEXT_PUBLIC_VELT_API_KEY=velt_live_xxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
VELT_AUTH_ENDPOINT=/api/auth/session
VELT_MAX_CONCURRENT_USERS=50
VELT_PRESENCE_DEBOUNCE_MS=33
// lib/velt-config.ts
import { VeltProviderConfig } from '@veltdev/react';
export const veltConfig: VeltProviderConfig = {
apiKey: process.env.NEXT_PUBLIC_VELT_API_KEY!,
authProvider: {
resolveUser: async () => {
const res = await fetch(process.env.VELT_AUTH_ENDPOINT!);
const data = await res.json();
return {
userId: data.id,
name: data.displayName,
color: data.preferredColor,
};
},
},
presence: {
debounceMs: parseInt(process.env.VELT_PRESENCE_DEBOUNCE_MS || '33', 10),
maxUsers: parseInt(process.env.VELT_MAX_CONCURRENT_USERS || '50', 10),
},
};
Quick Start Guide
- Initialize project: Run
npx create-next-app@latest collab-canvas --typescript --tailwind --app. Install dependencies: npm install @veltdev/react.
- Configure environment: Add
NEXT_PUBLIC_VELT_API_KEY to .env.local. Create lib/velt-config.ts with the template above.
- Wrap application: Import
CollabWorkspaceProvider in app/layout.tsx. Pass a dynamic workspaceId from URL params or backend session.
- Add canvas layer: Implement
PresenceOverlay and SpatialAnnotationOverlay alongside your HTML5 Canvas component. Pass viewport state and canvas ref to both overlays.
- Test collaboration: Open two browser windows with different
?user= query parameters. Draw, zoom, pan, and add annotations. Verify cursor synchronization and comment persistence across sessions.
This architecture delivers deterministic real-time synchronization, spatially accurate presence tracking, and production-ready annotation workflows. By abstracting WebSocket management and CRDT merge logic into a managed layer, engineering teams can focus on canvas UX, interaction design, and feature differentiation rather than distributed state plumbing.