DEV Community

Cover image for Secure Role-Based PDF Annotation in React: Filter, Lock, and Collaborate
Zahra Sandra Nasaka for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

Secure Role-Based PDF Annotation in React: Filter, Lock, and Collaborate

TL;DR: Learn how to implement role-based PDF annotation in React. This guide walks you through filtering annotations by user, locking non-author edits, and enabling secure collaborative workflows using a PDF viewer component.

Managing PDF annotations in team projects often gets messy, unstructured comments, unclear responsibilities, and security risks can derail collaboration. A role-based approach solves these challenges by linking each annotation to a specific user and assigning permissions based on roles, so you control who can add, view, or edit comments. This keeps feedback organized and prevents unauthorized changes.

In this guide, we’ll show you how to build a secure, role-based PDF annotation system in React to streamline collaboration and enhance team productivity.

Steps to integrate a Syncfusion PDF Viewer in React

To start adding annotations in React, you first need to set up the Syncfusion PDF Viewer in your application. Follow these steps:

Step 1: Initialize your React project

Start by creating a new React app using the Create React App command:

npx create-react-app syncfusion-pdf-viewer
cd syncfusion-pdf-viewer
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Syncfusion React PDF Viewer

Install the required Syncfusion React PDF Viewer library to bring all its capabilities into your React project. This package includes all the necessary modules for rendering PDFs:

npm install @syncfusion/ej2-react-pdfviewer
Enter fullscreen mode Exit fullscreen mode

Step 3: Import required styles

Next, import the required CSS files into your PdfViewer.css to ensure proper styling:

   @import '../node_modules/@syncfusion/ej2-base/styles/material.css';  
   @import '../node_modules/@syncfusion/ej2-buttons/styles/material.css';
   @import '../node_modules/@syncfusion/ej2-dropdowns/styles/material.css';  
   @import '../node_modules/@syncfusion/ej2-inputs/styles/material.css';  
   @import '../node_modules/@syncfusion/ej2-navigations/styles/material.css';
   @import '../node_modules/@syncfusion/ej2-popups/styles/material.css';
   @import '../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css';
   @import "../node_modules/@syncfusion/ej2-pdfviewer/styles/material.css";
Enter fullscreen mode Exit fullscreen mode

Step 4. Add the Syncfusion React PDF Viewer

Then, add the Syncfusion PDF Viewer to your React app:

import './PdfViewer.css';
import {
  PdfViewerComponent,
  Toolbar,
  Magnification,
  Navigation,
  LinkAnnotation,
  BookmarkView,
  ThumbnailView,
  Print,
  TextSelection,
  Annotation,
  TextSearch,
  FormFields,
  FormDesigner,
  Inject,
} from '@syncfusion/ej2-react-pdfviewer';

function App() {
  return (
    <div>
      <div className="control-section">
        {/* Render the PDF Viewer */}
        <PdfViewerComponent
          id="container"
          documentPath="https://cdn.syncfusion.com/content/pdf/pdf-succinctly.pdf"
          resourceUrl="https://cdn.syncfusion.com/ej2/31.2.7/dist/ej2-pdfviewer-lib"
          style={{ height: '640px' }}
        >
          <Inject
            services={[
              Toolbar,
              Magnification,
              Navigation,
              Annotation,
              LinkAnnotation,
              BookmarkView,
              ThumbnailView,
              Print,
              TextSelection,
              TextSearch,
              FormFields,
              FormDesigner,
            ]}/>
        </PdfViewerComponent>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now that your React app is set up with Syncfusion PDF Viewer and user authentication, let’s move on to implementing role-based annotation controls to ensure secure and organized collaboration

User identification with Authentication

To keep annotations secure and personalized, start with a simple authentication flow. When users open the app, they’re greeted with a login page where they can sign in or create an account.

Once logged in, they access the PDF Viewer and work exclusively with their own annotations. Every comment is tied to their account, so users only see and edit their own notes, keeping feedback organized and accountability clear.

Whether you’re building a document review tool or a file-sharing platform, authentication ensures that annotations are user-specific, secure, and easy to manage.

// Authentication: Login/Register UI. Posts to /login or /register and calls onLogin(user) on success.

function Authentication({ onLogin }) {
  const [isRegistering, setIsRegistering] = useState(false);
  const [message, setMessage] = useState("");
  const [username, setUsername] = useState("");

  const validateEmail = (email) => /\S+@\S+\.\S+/.test(email);

  const handleLogin = async (event) => {
    event.preventDefault();
    setMessage("");

    const form = event.currentTarget;
    const email = form.elements.email.value.trim();
    const password = form.elements.password.value.trim();

    if (!validateEmail(email)) {
      setMessage(" Please enter a valid email.");
      return;
    }

    try {
      const response = await fetch(`${hostURL}/login`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });

      if (response.ok) {
        const userData = await response.json();
        localStorage.setItem("user", JSON.stringify(userData));
        onLogin(userData);
      } else {
        setMessage(" Invalid email or password.");
      }
    } catch {
      setMessage(" Server error during login.");
    }
  };

  const handleRegister = async (e) => {
    e.preventDefault();
    setMessage("");

    const form = e.currentTarget;
    const email = form.elements.email.value;
    const password = form.elements.password.value;

    if (!validateEmail(email)) {
      setMessage(" Please enter a valid email.");
      return;
    }

    try {
      const register = await fetch(`${hostURL}/register`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ username, email, password }),
      });

      const data = await register.json();
      if (register.ok) {
        setMessage(" Registration successful. You can now log in.");
        setIsRegistering(false);
        setUsername("");
      } else {
        setMessage(`${data.message || "Registration failed"}`);
      }
    } catch {
      setMessage(" Server error during registration.");
    }
  };

  return (
    <div className="auth-container">
      <h2 className="auth-title">{isRegistering ? "Register" : "Login"}</h2>
      <form
        className="auth-form"
        onSubmit={isRegistering ? handleRegister : handleLogin}>
        {isRegistering && (

            <label className="auth-label" htmlFor="username">
              Username:
            </label>
            <input
              id="username"
              type="text"
              name="username"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
              required
              className="auth-input"
              placeholder="Your username"/>

        )}
        <label className="auth-label" htmlFor="email">
          Email:
        </label>
        <input
          id="email"
          type="email"
          name="email"
          required
          className="auth-input"
          placeholder="you@example.com"/>
        <label className="auth-label" htmlFor="password">
          Password:
        </label>
        <input
          id="password"
          type="password"
          name="password"
          required
          className="auth-input"
          placeholder="Your password"/>
        <button type="submit" className="auth-button primary">
          {isRegistering ? "Register" : "Login"}
        </button>
      </form>
      <button
        className="auth-button secondary"
        onClick={() => {
          setIsRegistering(!isRegistering);
          setMessage("");
          setUsername("");
        }}
        style={{ marginTop: 12 }}
      >
        {isRegistering
          ? "Already have an account? Log in"
          : "Don't have an account? Register"}
      </button>
      {message && <p className="auth-message">{message}</p>}
    </div>
  );
}

export default Authentication;
Enter fullscreen mode Exit fullscreen mode

With this code, your React app renders the Syncfusion PDF Viewer alongside a login page, enabling authenticated access to personalized annotation features for safe and collaborative document workflows.

Login or register UI for personalized annotations


Login or register UI for personalized annotations

Show or hide annotation based on the user

Controlling annotation visibility is key to creating a clean, personalized experience. With the Syncfusion PDF Viewer’s APIs, such as deleteAnnotations, importAnnotation, and exportAnnotationsAsObject, you can dynamically manage which comments appear for each user.

Here’s how it works: export all annotations, clear them from the view, and then re-import only those that match the logged-in user’s filter. This ensures every user sees only their own notes, keeping collaboration organized and secure.

By leveraging these APIs, you gain full control over annotation workflows, making document reviews more structured, user-specific, and scalable for team projects.

/**
 * Filters annotations by user and imports them into the viewer.
 * - If userName is 'All Authors', import everything.
 * - Otherwise, import only annotations where title === userName.
 */

const filterAndLoadAnnotations = (userName, allAnnotations) => {
  if (!pdfViewerRef.current) return;
  setTimeout(() => {
    try {
      pdfViewerRef.current.deleteAnnotations();
    } catch {}
    if (!allAnnotations) return;
    try {
      if (userName === 'All Authors') {
        let parsedData =
          typeof allAnnotations === 'string' && allAnnotations.trim() !== ''
            ? JSON.parse(allAnnotations)
            : allAnnotations;
        if (!parsedData) return;
        const dataToImport = parsedData.pdfAnnotation ? parsedData : { pdfAnnotation: parsedData };
        pdfViewerRef.current.importAnnotation(dataToImport);
        return;
      }
      let parsedData =
        typeof allAnnotations === 'string' ? JSON.parse(allAnnotations) : allAnnotations;
      if (!parsedData?.pdfAnnotation) parsedData = { pdfAnnotation: parsedData };
      const filteredData = { pdfAnnotation: {} };
      Object.keys(parsedData.pdfAnnotation).forEach(pageNumber => {
        const pageData = parsedData.pdfAnnotation[pageNumber];
        const list = pageData?.shapeAnnotation || [];
        const userAnnotations = list.filter(a => a.title === userName);
        if (userAnnotations.length > 0) {
          filteredData.pdfAnnotation[pageNumber] = { shapeAnnotation: userAnnotations };
        }
      });
      if (Object.keys(filteredData.pdfAnnotation).length > 0) {
        pdfViewerRef.current.importAnnotation(filteredData);
      }
    } catch (error) {
      console.error('Error filtering or loading annotations:', error);
    }
  }, 0);
};
Enter fullscreen mode Exit fullscreen mode

View all annotations with “All Authors” mode

Need a complete picture of team feedback? The “All Authors” mode displays every annotation in the document, giving reviewers full context for collaborative decisions. This is ideal for team-wide reviews where seeing all comments and markups helps maintain clarity and transparency.

All Authors” view showing annotations from all users


All Authors” view showing annotations from all users

Filter annotations by selected user

Sometimes, you need focus. By selecting a specific user from the dropdown, the PDF Viewer filters annotations to show only that person’s comments. This creates a distraction-free review experience, perfect for checking individual feedback without the noise of other contributors.

Filtering annotations to show only the selected user’s comments


Filtering annotations to show only the selected user’s comments

Add annotations based on the logged-in user

The annotation toolbar is smart; it is enabled only when the dropdown is set to “All Authors” or the logged-in user. This prevents unauthorized edits and keeps annotation integrity intact. When a user adds a new note, the system automatically assigns their name as the author, ensuring accountability and proper tracking.

/**
 * When a new annotation is added:
 * - Stamp the author as the current displayName
 * - Export annotations and collect the ones related to the new item
 * - Merge them into the global store so filters remain in sync
 */

const onAnnotationAdd = async (args) => {
  const viewer = pdfViewerRef.current;
  const coll = viewer.annotationCollection || [];
  const addedAnnotation = coll[coll.length - 1];
  if (!addedAnnotation) return;
  addedAnnotation.author = displayName;
  viewer.annotation.editAnnotation(addedAnnotation);
  const exportedData = await viewer.exportAnnotationsAsObject();
  const allAnnnots = JSON.parse(exportedData).pdfAnnotation;
  const exportData = [];
  if (allAnnnots) {
    const keys = Object.keys(allAnnnots);
    for (let x = 0; x < keys.length; x++) {
      const pageAnnots = allAnnnots[keys[x]].shapeAnnotation || [];
      for (let y = 0; y < pageAnnots.length; y++) {
        const pageAnnot = pageAnnots[y];
        if (pageAnnot.name === args.annotationId || pageAnnot.inreplyto === args.annotationId) {
          exportData.push(pageAnnot);
        }
      }
    }
  }
  combineAnnotations(exportData);
};

function combineAnnotations(exportData) {
  const existingData = JSON.parse(globalAnnotationsData);
  const key = exportData[0].page;
  if (existingData.pdfAnnotation[key]) {
    if (Array.isArray(existingData.pdfAnnotation[key].shapeAnnotation)) {
      for (let x = 0; x < exportData.length; x++) {
        existingData.pdfAnnotation[key].shapeAnnotation.push(exportData[x]);
      }
    } else {
      const keysLength = Object.keys(existingData.pdfAnnotation[key].shapeAnnotation).length;
      for (let x = 0; x < exportData.length; x++) {
        existingData.pdfAnnotation[key].shapeAnnotation[(keysLength + x).toString()] = exportData[x];
      }
    }
  } else {
    existingData.pdfAnnotation[key] = { shapeAnnotation: {} };
    for (let x = 0; x < exportData.length; x++) {
      existingData.pdfAnnotation[key].shapeAnnotation[x.toString()] = exportData[x];
    }
  }
  localStorage.setItem('annot', JSON.stringify(existingData));
  const combinedAnnotations = JSON.stringify(existingData);
  setGlobalAnnotationsData(combinedAnnotations);
Enter fullscreen mode Exit fullscreen mode

Save annotations with logged-in user as author

When the logged-in user adds a new annotation, it’s automatically saved with their username as the author. This ensures clear ownership, accountability, and a personalized experience for every contributor.

New annotation automatically saved with logged-in user as author


New annotation automatically saved with logged-in user as author

Disable annotation editing for other users

To maintain document integrity, editing is restricted to the original author. If a different user is selected from the dropdown, the “Add or Edit Annotation” option is disabled. This prevents unauthorized changes and accidental edits during collaborative reviews.

Editing disabled for other users’ annotations


Editing disabled for other users’ annotations

Edit annotations based on the logged-in user:

Every annotation is tagged with the logged-in user’s name for transparency. To enforce security, only the author can edit their annotations—others remain locked. This is achieved using the isLock property:

  • If the logged-in user is the author, isLock is set to false, allowing edits.
  • If not, isLock is set to true, keeping the annotation protected.

This approach ensures organized collaboration, proper tracking, and document integrity throughout multi-user reviews.

/**
 * Lock/unlock annotations on import based on simple ownership rules:
 * - Author == logged-in user AND selected author == logged-in user -> unlocked
 * - Else lock the annotation
 */

function lockAnnnotations() {
  var annotations = pdfViewerRef.current.annotationCollection;
  for (let i = 0; i < annotations.length; i++) {
    const annot = annotations[i];
    if (annot.author === loggedInUser.username && selectedAuthor === loggedInUser.username) {
      annot.annotationSettings.isLock = false;
    } else if (annot.author === "Guest" && selectedAuthor === "All Authors") {
      annot.annotationSettings.isLock = false;
    } else {
      annot.annotationSettings.isLock = true;
    }
    pdfViewerRef.current.annotation.editAnnotation(annotations[i]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Annotations editable only by their author


Annotations editable only by their author

Key benefits of user-based annotation support

  • Personalized annotation control: Every annotation is linked to the logged-in user, ensuring clear authorship and preventing unauthorized edits.
  • Smart annotation filtering: Switch between viewing all annotations or just your own, reducing clutter and focusing on relevant feedback.
  • Developer-friendly API integration: Syncfusion offers intuitive APIs to add, filter, lock, and manage annotations for flexible implementation.
  • Flexible & scalable for modern applications: Works across React, Angular, Vue, and Blazor. It supports role-based access control and read-only modes, making it ideal for secure, scalable enterprise applications.

GitHub reference

To explore the full annotation implementation on GitHub and try it yourself.

Conclusion

Syncfusion’s React PDF Viewer is a powerful solution for building user-specific PDF annotation workflows in React. Unlike standard PDF editors, it provides developers fine-grained control over annotation of visibility and editing, ensuring that each user interacts only with their own content.

With features like author tagging, role-based access, and secure editing logic, you can easily implement functionality such as filtering annotations by author, locking annotations for non-authors, and managing dynamic annotation layers.

If you’re developing a document review tool, legal workflow, or any application that requires personalized PDF annotations in React, Syncfusion’s PDF Viewer delivers clarity, control, and accountability, without the clutter.

If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!

Related blogs

This article was originally published at Syncfusion.com.

Top comments (0)