Skip to main content
Configure SuperDoc on the client-side to enable real-time collaborative editing with multiple users.

Quick Start

import { SuperDoc } from 'superdoc';

const superdoc = new SuperDoc({
  selector: '#editor',
  document: {
    id: 'doc-123',
    type: 'docx',
    isNewFile: false,
  },
  user: {
    name: 'John Smith',
    email: '[email protected]'
  },
  modules: {
    collaboration: {
      url: 'ws://localhost:3050/doc',
      token: 'auth-token'
    }
  }
});

Configuration Options

Basic Setup

modules.collaboration
Object
required
Collaboration module configuration
modules.collaboration.url
string
required
WebSocket URL for the collaboration server
// For production
url: 'wss://collab.example.com/doc'

// For development  
url: 'ws://localhost:3050/doc'

// With dynamic document routing
url: `${wsUrl}/doc`
modules.collaboration.token
string
Authentication token for server validation
token: 'jwt-token-here'
// or
token: await getAuthToken()
modules.collaboration.params
Object
Additional connection parameters sent to the server. These will be exposed in server-side collaborationParams.
params: {
  apiKey: 'your-api-key',
  projectId: 'project-123',
  customParam: 'value'
}
modules.collaboration.providerType
string
default:"'superdoc'"
WebSocket provider implementation to use
  • 'superdoc' - Default WebSocket provider using y-websocket
  • 'hocuspocus' - Hocuspocus provider for advanced features
providerType: 'hocuspocus' // or 'superdoc'
modules.collaboration.preserveConnection
boolean
default:"false"
Whether to preserve WebSocket connection (Hocuspocus provider only)
preserveConnection: true // Keep connection alive

Document Configuration

document
Object
required
Document configuration for collaboration
document.id
string
required
Unique document identifier - used as collaboration room ID
document: {
  id: route.params.documentId, // From URL params
  type: 'docx',
  isNewFile: false
}
document.type
string
required
Document type - currently supports 'docx'
document.isNewFile
boolean
Whether this is a new document or loading existing content

User Configuration

user
Object
required
Current user information for collaboration awareness
user.name
string
required
Display name shown to other users
user.email
string
required
Unique user identifier (used for user presence)
user.image
string
Avatar URL for user presence display

User Presence Colors

colors
string[]
Color palette for user cursors and selections
colors: [
  '#a11134', // Red
  '#2a7e34', // Green  
  '#b29d11', // Yellow
  '#2f4597', // Blue
  '#ab5b22'  // Brown
]
Users automatically receive unique colors from this palette.

Event Handlers

onReady Event

Triggered when SuperDoc and collaboration are fully initialized.
onReady
function
Called when SuperDoc is ready and collaboration is connected
onReady: (event) => {
  console.log('SuperDoc is ready', event);
  const editor = event.superdoc.activeEditor;
  
  // Access the Yjs document for custom functionality
  const ydoc = event.superdoc.ydoc;
  if (ydoc && editor) {
    setupCustomObservers(ydoc, editor);
  }
}
event.superdoc
SuperDoc
The SuperDoc instance
event.superdoc.activeEditor
Object
The active ProseMirror editor instance
event.superdoc.ydoc
Y.Doc
The Yjs document for collaboration state

onAwarenessUpdate Event

Handle user presence updates (users joining/leaving).
onAwarenessUpdate
function
Called when user awareness state changes
const onAwarenessUpdate = ({ states, added, removed, superdoc }) => {
  // Handle removed users first
  if (removed && removed.length > 0) {
    removed.forEach(clientId => {
      const index = connectedUsers.value.findIndex(user => user.clientId === clientId);
      if (index !== -1) {
        console.log("Removing user:", connectedUsers.value[index]);
        connectedUsers.value.splice(index, 1);
      }
    });
  }

  // Handle added users
  if (added && added.length > 0) {
    added.forEach(clientId => {
      const userState = states.find(user => user.clientId === clientId);
      if (userState && userState.name && !connectedUsers.value.find(u => u.clientId === clientId)) {
        console.log("Adding user:", userState);
        connectedUsers.value.push(userState);
      }
    });
  }

  // Fallback: initial load when no added/removed events
  if ((!added || added.length === 0) && (!removed || removed.length === 0)) {
    if (states) {
      connectedUsers.value = states.filter(user => user && user.name);
      console.log("Initial user load:", connectedUsers.value);
    }
  }
};
event.states
Array
Array of all current user awareness states with their information
event.added
number[]
Array of client IDs for users that joined
event.removed
number[]
Array of client IDs for users that left
event.superdoc
SuperDoc
Reference to the SuperDoc instance

User State Properties

Each user object in the states array contains:
clientId
number
Unique client identifier for this user session
name
string
Display name of the user
email
string
User’s email address (unique identifier)
color
string
Hex color assigned to this user for cursors and selections
cursor
Object
Current cursor position information (if available)
selection
Object
Current text selection information (if available)

onEditorCreate Event

onEditorCreate
function
Called when each editor instance is created.This can be used do things like load a default document if the current document loaded from a server is blank.
onEditorCreate: async (event) => {
  const { editor } = event;

  if (!editor?.state) return;
  const textContent = editor.state.doc.textContent;

  // Check if document is empty (no content or only whitespace)
  const isEmpty = !textContent || textContent.trim().length === 0;
  if (!isEmpty) return;

  try {
    // Fetch and load default.docx
    const response = await fetch(defaultDocument);
    const blob = await response.blob();
    const file = new File([blob], 'default.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });

    await editor.replaceFile(file);
  } catch (error) {
    console.error('Error loading default content:', error);
  }
}

onContentError Event

onContentError
function
Called when document content fails to load
onContentError: ({ error, documentId, file }) => {
  console.error('Content loading error:', error);
  showErrorToUser('Failed to load document');
}

Media Upload Integration

Handle image and media uploads in collaborative documents.
handleImageUpload
function
Custom image upload handler for collaboration
const handleImageUpload = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    
    reader.onload = (event) => {
      const dataUrl = event.target.result;
      const mediaPath = `word/media/${file.name}`;

      // Store in Yjs shared state for collaboration
      if (superdoc.value?.ydoc) {
        const mediaMap = superdoc.value.ydoc.getMap('media');
        mediaMap.set(mediaPath, dataUrl);
      }
    
      resolve(dataUrl);
    };
    
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

Complete Example

Here’s a complete Vue.js example based on the production implementation:
<script setup>
import { SuperDoc } from 'superdoc';
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';

const superdoc = shallowRef(null);
const connectedUsers = ref([]);
const currentUser = ref(null);

const generateUserInfo = async () => {
  const response = await fetch('/api/user');
  return await response.json();
};

const handleImageUpload = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const dataUrl = event.target.result;
      const mediaPath = `word/media/${file.name}`;

      if (superdoc.value?.ydoc) {
        const mediaMap = superdoc.value.ydoc.getMap('media');
        mediaMap.set(mediaPath, dataUrl);
      }
    
      resolve(dataUrl);
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

const onAwarenessUpdate = ({ states }) => {
  const activeUsers = Array.from(states.values())
    .filter(state => state.user && state.user.name);
  connectedUsers.value = activeUsers;
};

const init = async () => {
  const documentId = 'my-document-id';
  const wsUrl = 'ws://localhost:3050';
  const user = await generateUserInfo();
  currentUser.value = user;

  superdoc.value = new SuperDoc({
    selector: '#superdoc',
    toolbar: '#superdoc-toolbar',
    document: {
      id: documentId,
      type: 'docx',
      isNewFile: false,
    },
    pagination: true,
    colors: ['#a11134', '#2a7e34', '#b29d11', '#2f4597', '#ab5b22'],
    user,
    modules: {
      collaboration: {
        url: `${wsUrl}/doc`,
        token: 'auth-token',
      },
    },
    handleImageUpload,
    onAwarenessUpdate,
    onReady: (event) => {
      const editor = event.superdoc.activeEditor;
      const ydoc = event.superdoc.ydoc;
      
      if (ydoc && editor) {
        setupMediaObserver(ydoc, editor);
      }
    },
    onContentError: ({ error, documentId }) => {
      console.error('Content loading error:', error, documentId);
    }
  });
};

onMounted(() => init());

onBeforeUnmount(() => {
  if (superdoc.value) {
    superdoc.value.destroy();
  }
});
</script>

<template>
  <div class="collaboration-container">
    <div id="superdoc-toolbar"></div>
    <div id="superdoc"></div>
    
    <div class="user-presence">
      <div v-for="user in connectedUsers" :key="user.email">
        {{ user.name }}
      </div>
    </div>
  </div>
</template>

Authentication Patterns

JWT Token Authentication

const getAuthToken = async () => {
  const response = await fetch('/api/auth/token', {
    credentials: 'include'
  });
  const { token } = await response.json();
  return token;
};

const superdoc = new SuperDoc({
  // ... other config
  modules: {
    collaboration: {
      url: 'wss://server.com/doc',
      token: await getAuthToken()
    }
  }
});

Dynamic Document URLs

// For multi-tenant applications
const documentId = route.params.documentId;
const orgId = route.params.orgId;

modules: {
  collaboration: {
    url: `wss://server.com/orgs/${orgId}/docs/${documentId}`,
    token: authToken
  }
}

Error Handling

// Handle authentication failures
superdoc.provider?.on('authenticationFailed', async ({ reason }) => {
  if (reason === 'token-expired') {
    const newToken = await refreshAuthToken();
    superdoc.provider.configuration.token = newToken;
    superdoc.provider.connect();
  } else {
    redirectToLogin();
  }
});

// Handle connection errors
superdoc.provider?.on('connectionError', ({ error }) => {
  console.error('Connection error:', error);
  showUserNotification('Connection failed. Retrying...');
});

Performance Considerations

  • Limit concurrent users: Keep under 50 active users per document for optimal performance
  • Efficient awareness updates: Only update UI when user list actually changes
  • Media handling: Compress images before storing in Yjs maps
  • Connection monitoring: Implement proper reconnection logic with exponential backoff

Next Steps

Set Up Backend

Configure your collaboration server to handle client connections