Add real-time collaboration to your Nuxt Crouton applications with the @fyit/crouton-collab package. Built on Yjs CRDTs for conflict-free synchronization, it supports rich text editing, flow graphs, and any collaborative data structure.
pnpm add @fyit/crouton-collab
Add the layer to your nuxt.config.ts:
export default defineNuxtConfig({
extends: [
'@fyit/crouton',
'@fyit/crouton-collab'
]
})
<script setup>
// For rich text editors (pages)
const { ydoc, yxmlFragment, connected, users } = useCollabEditor({
roomId: 'page-123',
roomType: 'page'
})
// For flow graphs
const { ymap, data, connected, users } = useCollabSync({
roomId: 'flow-123',
roomType: 'flow',
structure: 'map'
})
</script>
<template>
<div>
<CollabIndicator
:connected="connected"
:synced="true"
:users="users"
/>
<!-- Your editor content -->
</div>
</template>
Ready-to-use setup for TipTap collaborative editing.
const {
// Connection state
connected, // Whether WebSocket is connected
synced, // Whether initial sync is complete
error, // Connection error, if any
// Yjs for TipTap
ydoc, // Y.Doc instance
yxmlFragment, // Y.XmlFragment for content
// Presence
user, // Current user
users, // All users in room
otherUsers, // Other users (excluding self)
// For TipTap extensions
provider, // { awareness: { setLocalStateField, ... } }
// Actions
connect, // Manually connect
disconnect, // Manually disconnect
updateCursor, // Update cursor position
updateSelection // Update text selection
} = useCollabEditor({
roomId: 'page-123',
roomType: 'page', // default
field: 'content', // default
user: { name: 'Alice', color: '#ff0000' }
})
TipTap Integration:
import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
const editor = useEditor({
extensions: [
Collaboration.configure({
document: ydoc,
field: 'content'
}),
CollaborationCursor.configure({
provider,
user: { name: 'Alice', color: '#ff0000' }
})
]
})
General-purpose Yjs synchronization for any data structure.
const {
// Connection state
connected, synced, error,
// Yjs structures (one populated based on structure option)
ymap, // Y.Map | null
yarray, // Y.Array | null
yxmlFragment, // Y.XmlFragment | null
ytext, // Y.Text | null
// Reactive data
data, // Ref<Record<string, unknown>> - for Y.Map
arrayData, // Ref<unknown[]> - for Y.Array
// Users in room
users,
// Actions
connect, disconnect
} = useCollabSync({
roomId: 'room-123',
roomType: 'flow',
structure: 'map', // 'map' | 'array' | 'xmlFragment' | 'text'
structureName: 'nodes' // optional, defaults to roomType
})
Track cursor positions, selections, and presence.
const {
user, // Current user
users, // All users
otherUsers, // Other users (excluding self)
// Actions
updateCursor, // (cursor) => void
updateSelection, // (selection) => void
selectNode, // (nodeId) => void
updateGhostNode, // (ghostNode) => void
// Utilities
getUsersSelectingNode, // (nodeId) => CollabAwarenessState[]
getUserColor, // (userId) => string
getNodePresenceStyle // (nodeId) => { boxShadow?, borderColor? }
} = useCollabPresence({
connection, // from useCollabConnection
user: { name: 'Alice' }
})
Poll room users via HTTP (for global presence in lists).
const {
users, // All users in room
otherUsers, // Excluding current user
count, // Total count
otherCount, // Other users count
loading,
error,
isPolling,
// Actions
refresh, // Manual refresh
startPolling, // Start polling
stopPolling // Stop polling
} = useCollabRoomUsers({
roomId: 'page-123',
roomType: 'page',
pollInterval: 5000, // default: 5 seconds
currentUserId: user?.id,
excludeSelf: true, // default
immediate: true // default: start on mount
})
Connection status indicator with colored dot.
<CollabStatus
:connected="connected"
:synced="synced"
:error="error"
:show-label="true"
/>
| Prop | Type | Default | Description |
|---|---|---|---|
connected | boolean | required | WebSocket connected |
synced | boolean | required | Initial sync complete |
error | Error | null | null | Connection error |
showLabel | boolean | true | Show text label |
Status Colors:
Stacked user avatars with overflow indicator.
<CollabPresence
:users="otherUsers"
:max-visible="5"
size="sm"
:show-tooltip="true"
/>
| Prop | Type | Default | Description |
|---|---|---|---|
users | CollabAwarenessState[] | required | Users to display |
maxVisible | number | 5 | Max avatars before +N |
size | 'xs' | 'sm' | 'md' | 'sm' | Avatar size |
showTooltip | boolean | true | Show name on hover |
Combined status + presence for toolbars.
<CollabIndicator
:connected="connected"
:synced="synced"
:error="error"
:users="otherUsers"
:max-visible-users="3"
/>
Shows "X editing" badge on collection list items.
<CollabEditingBadge
room-id="page-123"
room-type="page"
:current-user-id="currentUser?.id"
:poll-interval="5000"
size="xs"
:show-avatars="true"
/>
| Prop | Type | Default | Description |
|---|---|---|---|
roomId | string | required | Room ID to check |
roomType | string | 'page' | Room type |
currentUserId | string | - | Exclude self from count |
pollInterval | number | 5000 | Poll interval in ms |
size | 'xs' | 'sm' | 'md' | 'xs' | Badge size |
showAvatars | boolean | true | Show avatars on hover |
Remote cursor overlay for canvas/editor content.
<div class="relative">
<CollabCursors
:users="otherUsers"
:show-labels="true"
:offset-x="0"
:offset-y="0"
/>
<!-- Your content here -->
</div>
Show "X people editing" badges in collection lists:
<CroutonCollection
collection="pages"
:rows="pages"
:show-collab-presence="true"
/>
<!-- With custom configuration -->
<CroutonCollection
collection="pages"
:rows="pages"
:show-collab-presence="{
roomType: 'page',
currentUserId: currentUser?.id,
pollInterval: 10000,
getRoomId: (row, collection) => `${collection}-${row.id}`
}"
/>
The collaboration package uses a layered architecture:
┌─────────────────────────────────────────────────────┐
│ Clients │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ User A │ │ User B │ │ User C │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └────────────┼────────────┘ │
│ │ WebSocket │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ /api/collab/[roomId]/ws │ │
│ │ ?type=page|flow|document │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ CollabRoom DO │ │
│ │ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ Y.Doc │ │ Sessions │ │ Awareness │ │ │
│ │ │ (CRDT) │ │(WebSockets)│ │(Presence) │ │ │
│ │ └────┬────┘ └──────────┘ └──────────────┘ │ │
│ └───────┼───────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ D1: yjs_collab_states │ │
│ │ room_type │ room_id │ state │ version │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
| Type | Use Case | Yjs Structure |
|---|---|---|
page | TipTap editor content | Y.XmlFragment |
flow | Node graphs | Y.Map |
document | Plain text | Y.Text |
generic | Custom | Any |
[[durable_objects.bindings]]
name = "COLLAB_ROOMS"
class_name = "CollabRoom"
[[migrations]]
tag = "collab-v1"
new_classes = ["CollabRoom"]
npx wrangler d1 execute <DB_NAME> \
--file=./packages/nuxt-crouton-collab/server/database/migrations/0001_yjs_collab_states.sql
Raw Uint8Array containing Yjs update data.
// Awareness update
{
type: 'awareness',
userId: 'user-123',
state: {
user: { id: 'user-123', name: 'Alice', color: '#ff0000' },
cursor: { x: 100, y: 200 },
selection: { anchor: 10, head: 20 }
}
}
// Ping/Pong for connection health
{ type: 'ping' }
{ type: 'pong' }
interface CollabUser {
id: string
name: string
color: string
}
interface CollabAwarenessState {
user: CollabUser
cursor: { x: number; y: number } | null
selection?: { anchor: number; head: number } | null
selectedNodeId?: string | null
ghostNode?: { id: string; position: { x: number; y: number } } | null
[key: string]: unknown // Extensible
}
interface CollabConnectionState {
connected: boolean
synced: boolean
error: Error | null
}
type CollabStructure = 'map' | 'array' | 'xmlFragment' | 'text'
| Endpoint | Method | Description |
|---|---|---|
/api/collab/[roomId]/ws?type=X | GET | WebSocket upgrade |
/api/collab/[roomId]/users?type=X | GET | Get current users |
{
"users": [
{
"user": { "id": "user-123", "name": "Alice", "color": "#ff0000" },
"cursor": { "x": 100, "y": 200 }
}
],
"count": 1
}
currentUserId to exclude the current usertype and roomId query params# Via curl (when server is running)
curl https://your-app.com/api/collab/room-123/users?type=page
| Error | Cause | Solution |
|---|---|---|
| WebSocket connection failed | Server not running | Check server logs |
| Room not found | Missing D1 migration | Run migration SQL |
| User not syncing | Wrong room type | Verify type parameter |