> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/collaboration/basics.md).

# Learn More About Document Collaboration in WebViewer

Learn how to enable real-time document collaboration using WebViewer's JavaScript library. Sync annotations between users with server-side support for seamless collaboration. Build your own server usi

WebViewer's JavaScript document collaboration library contains APIs that allow you to export/import annotations from/to a document. Using those APIs and a server, you can set up realtime collaboration easily.

{% hint style="info" %}
Apryse does not ship any server side collaboration tools - you must take the basic concepts outlined below and use them to implement your own flow within your application.
{% endhint %}

## Technologies

Enabling collaboration in WebViewer requires you to build a server that can sync annotations back and forth between users. This typically involves storing annotation data ([XFDF](/web/annotation/import-export.md)) in a database and enabling some kind of real-time connection between your clients and your server. This is typically accomplished through the use of Web Sockets.

You can use any backend technologies you wish. We provide samples using SQLite and Firebase, but collaboration is not restricted to these technologies.

## Collaboration basics

### Syncing annotations (client side)

Setting up real time collaboration typically involves binding to the WebViewer `annotationChanged` event, and syncing the clients annotation state with your servers annotation state.

This usually will look something like this

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { annotationManager } = instance.Core;
    annotationManager.addEventListener('annotationChanged', (annotations, action) => {
      
      // It is recommended to assign a unique ID to each document
      const documentId = getDocumentId()
      
      // Sync annotations with server depending on the action
      if (action === 'add') {
        await addAnnotations(annotations, documentId)
      } else if (action === 'modify') {
        await modifyAnnotations(annotations, documentId)
      } else if (action === 'delete') {
        await deleteAnnotations(annotations, documentId)
      }
    });
  }))
```

{% endcode %}
{% endtab %}
{% endtabs %}

The addAnnotations, modifyAnnotations, and deleteAnnotations functions will typically contain your own logic to extract the XFDF from the changed annotations and push the updates to your server.

Here's an example of what an `addAnnotation` function might look like.

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
async function addAnnotations(annotations, annotationManager, documentId) {
    const payload = [];
    for(const annotation of annotations) {
        const xfdf = await annotationManager.exportAnnotations({ annotList: [annotation] })
        payload.push({
            xfdf,
            id: annotation.Id,
            documentId
        })
    }
    // Sync the new annotations to your server / database
    await fetch("/api/annotations", {
        body: JSON.stringify(payload),
        method: "POST"
    })
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
It is recommended to assign a unique ID to each document in your system. This will allow you to easily fetch and sync annotations on a per-document basis.

You should generate these ID's with your own system instead of using the ID generated by WebViewer.
{% endhint %}

### Syncing annotations (server side)

Once you have your client sending XFDF data to your server, you need to update your server to handle that data. This will typically involve setting up endpoints for each annotation operation (add, modify, delete).

Each of these endpoints should have 3 primary functions:

1. Authenticate the user
2. Sync the annotation to the database
3. Send real-time updates to other connected users

User authentication will depend on your server technology, so that will not be explained in this guide.

Syncing the annotation to your database will usually mean writing the XFDF, the userId, the documentId, and an annotationId.

If you are using an ORM like Prisma, this could look like this:

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
app.post('/annotations', (req, res) => {
    const annotations = req.body;
    
    for(const annotation of annotations) {
        await prisma.Annotation.create({
            data: {
                id: annotation.id,
                xfdf: annotation.xfdf,
                userId: req.user.id,
                documentId: annotation.documentId
            }
        })
    }
    // Send real-time events here
    // syncClients()
    
    res.send(200)  
}))
```

{% endcode %}
{% endtab %}
{% endtabs %}

The same logic applies for modifying data, except instead of writing a new row to your database, you will be editing or deleting an existing row.

### Real-time syncing between users

Once you have your basic annotation sync in place, you can start implementing the real-time updates between users.

There are many strategies you can use to accomplish this, but the most common are

* [WebSockets](https://en.wikipedia.org/wiki/WebSocket)
* [Polling](https://medium.com/@sujoy.swe/request-polling-with-examples-b31d0860d518)

If possible, we recommend using WebSockets as they provide a true real-time user experience.

The general strategy to implementing real-time updates is as follows

1. Each client that is viewing a document subscribes to a "topic", usually identified by the document ID
2. Any time your server receives a request to add/edit/delete annotations, it pushes an event to that topic. The payload for the event should contain the updated XFDF. These events should then fan out to your clients.
3. When your client receives an event from the server, it should take the payload and use WebViewer APIs to sync the new annotation with the viewer

Here is a code sample using [socket.io](https://socket.io/) to show what this might look like:

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
const socket = io();
WebViewer({
...
}, document.getElementById('viewer'))
  .then(instance => {
    const { documentViewer, annotationManager } = instance.Core;
    documentViewer.addEventListener('documentLoaded', () => {
        // Join the room for this document
        socket.join(documentId)
        socket.on("annotationAdded", (annotation) => {
            const { xfdf } = annotation;
            annotationManager.importAnnotations(xfdf)
        })
    })
  });

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
const io = require("socket.io")(httpServer, {
  // ...
});
app.post('/annotations', (req, res) => {
    const annotations = req.body;
    
    for(const annotation of annotations) {
        await prisma.Annotation.create({
            data: {
                id: annotation.id,
                xfdf: annotation.xfdf,
                userId: req.user.id,
                documentId: annotation.documentId
            }
        })
        
        // Send event to anyone subscribed to this documents topic
        io.to(annotation.documentId).emit("annotationAdded", annotation);
    }
    res.send(200)  
})

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Loading annotations on document load

The final piece of functionality for a basic real-time collaboration flow is displaying a document's annotations on load.

This is done by querying all the annotations belonging to the document from your database, and importing the XFDF for those annotations into WebViewer.

Here is an example of what this might look like:

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
WebViewer(...)
  .then(instance => {
    const { documentViewer, annotationManager } = instance.Core;
    documentViewer.addEventListener('documentLoaded', async () => {
        const documentId = getDocumentId() // get your doc ID here
        const resp = await fetch(`/api/annotations?docId=${documentId}`)
        const data = await resp.json()
        for(const annotation of data) {
            const { xfdf } = annotation;
            annotationManager.importAnnotations(xfdf)
        }
    });
  })
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="JavaScript" %}
{% code lineNumbers="true" %}

```js
app.get("/annotations", (req, res) => {
    const { docId } = req.query;
    const annots = await prisma.Annotations.findMany({
        where: {
            documentId: docId
        }
    })
    return res.json(annots)
})
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Next steps

The rest of the guides in this section walk you through how to implement a real-time collaboration flow using Firebase using the same concepts we discussed above.

Get started by [setting up your viewer](/web/collaboration/realtime-collaboration-setup.md).

{% hint style="info" %}
If you are not using firebase, we still recommend reading the remaining guides as the same concepts with almost any technology stack.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/collaboration/basics.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
