> 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/get-started/samples/realtime-collaboration.md).

# Realtime collaboration - Firebase

Enable real-time collaboration on PDF documents using JavaScript. Highlight text, circle objects, make notes & respond to comments directly in the browser.

Enable real-time collaboration on PDF, DOCX, PPTX and XLSX documents using this JavaScript sample. When a user creates a new annotation it will immediately be displayed in another user’s browser, where they can reply to annotations in real-time by adding their own comments. This sample works on all browsers (including IE11) and mobile devices without using plug-ins. For more details, refer to our [real-time collaboration guide](/web/collaboration/realtime-collaboration.md) or visit our [collaboration demo](https://showcase.apryse.com/document-collaboration/). Note: this example is setup with a Firebase backend, but you can use whichever backend you prefer. Learn more about our [Web SDK](/web/get-started/guides.md).

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

```js
const IDS = {
  'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf': 'foo-12',
  'https://pdftron.s3.amazonaws.com/downloads/pl/report.docx': 'foo-13',
  'https://pdftron.s3.amazonaws.com/downloads/pl/presentation.pptx': 'foo-14',
};

// eslint-disable-next-line no-undef
const server = new Server();
const initialDoc = 'https://pdftron.s3.amazonaws.com/downloads/pl/demo-annotated.pdf';

// NOTE: not for production use, delete this function when in production
const periodicallyRemoveOldAnnotations = data => {
  // "1 * 60 * 60 * 1000" is one hour
  const twentyFourHours = 24 * 60 * 60 * 1000;
  const item = data.val();
  if (!item.timestamp) {
    return false;
  }
  const now = Date.now();
  const passedTime = now - item.timestamp;
  // If the timestamp of the annotation is passed
  // 24 hours we delete this annotation here.
  if (passedTime > twentyFourHours) {
    server.deleteAnnotation(data.key);
    return true;
  }
  return false;
};

// eslint-disable-next-line no-undef
const WebViewerConstructor = isWebComponent() ? WebViewer.WebComponent : WebViewer;

WebViewerConstructor(
  {
    path: '../../../lib',
    initialDoc,
    documentId: IDS[initialDoc],
  },
  document.getElementById('viewer')
).then(instance => {
  samplesSetup(instance);

  const { documentViewer, annotationManager } = instance.Core;

  let authorId = null;
  const urlInput = document.getElementById('url');
  const copyButton = document.getElementById('copy');
  instance.UI.openElements(['notesPanel']);

  let hasSeenPopup = false;

  if (window.location.origin === 'http://localhost:3000') {
    const xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = () => {
      if (xhttp.readyState === 4 && xhttp.status === 200) {
        urlInput.value = `http://${xhttp.responseText}:3000/samples/annotation/realtime-collaboration/`;
      }
    };
    xhttp.open('GET', '/ip', true);
    xhttp.send();
  } else {
    urlInput.value = 'https://docs.apryse.com/samples/web/samples/annotation/realtime-collaboration/';
  }

  copyButton.onclick = () => {
    urlInput.select();
    document.execCommand('copy');
    document.getSelection().empty();
  };

  documentViewer.addEventListener('documentLoaded', () => {
    const documentId = documentViewer.getDocument().getDocumentId();

    server.selectDocument(documentId);

    const onAnnotationCreated = async data => {
      // NOTE: not for production use, delete this "if block" when in production
      if (periodicallyRemoveOldAnnotations(data)) {
        return;
      }

      // Import the annotation based on xfdf command
      const annotations = await annotationManager.importAnnotationCommand(data.val().xfdf);
      const annotation = annotations[0];
      if (annotation) {
        await annotation.resourcesLoaded();
        // Set a custom field authorId to be used in client-side permission check
        annotation.authorId = data.val().authorId;
        annotationManager.redrawAnnotation(annotation);
        // viewerInstance.fireEvent('updateAnnotationPermission', [annotation]); //TODO
      }
    };

    const onAnnotationUpdated = async data => {
      // Import the annotation based on xfdf command
      const annotations = await annotationManager.importAnnotationCommand(data.val().xfdf);
      const annotation = annotations[0];
      if (annotation) {
        await annotation.resourcesLoaded();
        // Set a custom field authorId to be used in client-side permission check
        annotation.authorId = data.val().authorId;
        annotationManager.redrawAnnotation(annotation);
      }
    };

    const onAnnotationDeleted = data => {
      // data.key would return annotationId since our server method is designed as
      // annotationsRef.child(annotationId).set(annotationData)
      const command = `<delete><id>${data.key}</id></delete>`;
      annotationManager.importAnnotationCommand(command);
    };

    const openReturningAuthorPopup = authorName => {
      if (hasSeenPopup) {
        return;
      }
      // The author name will be used for both WebViewer and annotations in PDF
      annotationManager.setCurrentUser(authorName);
      // Open popup for the returning author
      window.alert(`Welcome back ${authorName}`);
      hasSeenPopup = true;
    };

    const updateAuthor = authorName => {
      // The author name will be used for both WebViewer and annotations in PDF
      annotationManager.setCurrentUser(authorName);
      // Create/update author information in the server
      server.updateAuthor(authorId, { authorName });
    };

    const openNewAuthorPopup = () => {
      // Open prompt for a new author
      const name = window.prompt('Welcome! Tell us your name :)');
      if (name) {
        updateAuthor(name);
      }
    };

    // Bind server-side authorization state change to a callback function
    // The event is triggered in the beginning as well to check if author has already signed in
    server.bind('onAuthStateChanged', user => {
      // Author is logged in
      if (user) {
        // Using uid property from Firebase Database as an author id
        // It is also used as a reference for server-side permission
        authorId = user.uid;
        // Check if author exists, and call appropriate callback functions
        server.checkAuthor(authorId, openReturningAuthorPopup, openNewAuthorPopup);
        // Bind server-side data events to callback functions
        // When loaded for the first time, onAnnotationCreated event will be triggered for all database entries
        server.bind('onAnnotationCreated', onAnnotationCreated);
        server.bind('onAnnotationUpdated', onAnnotationUpdated);
        server.bind('onAnnotationDeleted', onAnnotationDeleted);
      } else {
        // Author is not logged in
        server.signInAnonymously();
      }
    });
  });

  // Bind annotation change events to a callback function
  annotationManager.addEventListener('annotationChanged', async (annotations, type, info) => {
    // info.imported is true by default for annotations from pdf and annotations added by importAnnotationCommand
    if (info.imported) {
      return;
    }

    const xfdf = await annotationManager.exportAnnotationCommand();
    // Iterate through all annotations and call appropriate server methods
    annotations.forEach(annotation => {
      let parentAuthorId = null;
      if (type === 'add') {
        // In case of replies, add extra field for server-side permission to be granted to the
        // parent annotation's author
        if (annotation.InReplyTo) {
          parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
        }

        if (authorId) {
          annotation.authorId = authorId;
        }

        server.createAnnotation(annotation.Id, {
          authorId,
          parentAuthorId,
          xfdf,
          timestamp: Date.now(),
        });
      } else if (type === 'modify') {
        // In case of replies, add extra field for server-side permission to be granted to the
        // parent annotation's author
        if (annotation.InReplyTo) {
          parentAuthorId = annotationManager.getAnnotationById(annotation.InReplyTo).authorId || 'default';
        }
        server.updateAnnotation(annotation.Id, {
          authorId,
          parentAuthorId,
          xfdf,
        });
      } else if (type === 'delete') {
        server.deleteAnnotation(annotation.Id);
      }
    });
  });

  // Overwrite client-side permission check method on the annotation manager
  // The default was set to compare the authorName
  // Instead of the authorName, we will compare authorId created from the server
  // Note that authorId can be undefined when annotation has just been created, need to handle it for fixing WVR-3217
  annotationManager.setPermissionCheckCallback((author, annotation) => !annotation.authorId || annotation.authorId === authorId);

  document.getElementById('select').onchange = e => {
    const documentId = IDS[e.target.value];
    instance.UI.loadDocument(e.target.value, { documentId });
  };
});
```

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


---

# 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/get-started/samples/realtime-collaboration.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.
