DEV Community

Cover image for Building Real-Time Collaborative Documents App with Velt and Auth0 in Next.js
Arindam Majumder
Arindam Majumder Subscriber

Posted on

Building Real-Time Collaborative Documents App with Velt and Auth0 in Next.js

Real-time collaboration features are important to have in modern web applications. While services like Google Docs and Figma make it look seamless, implementing these features from scratch involves complex challenges, such as operational transforms, conflict resolution, and state synchronization.

This tutorial demonstrates how to leverage Velt’s battle-tested collaboration infrastructure alongside Auth0's authentication system within a Next.js application.

We'll build a real-time collaborative document editor using the following:

  • Next.js - A React framework for building server-rendered applications
  • Auth0 - For robust authentication and user management
  • Velt SDK - An SDK for real-time collaboration features
  • Quill - A free open source WYSIWYG text editor package

The final application will enable authenticated users to:

  • Collaborate on documents with real-time synchronization
  • Add contextual comments through text selection
  • Receive notifications for comments and mentions
  • View a live list of collaborators currently editing the document

By using Velt you are able to focus on building your application without worrying about the complexities of real-time collaboration.

Why combine Velt SDK with Auth0 in a Next.js app?

Next.js + Auth0 provides:

  • Type-safe API routes with middleware support
  • Secure session management
  • Built-in OAuth flows and social login options
  • Efficient server-side rendering for faster initial loads

Velt SDK eliminates the need to:

  • Implement WebSocket connections and state sync
  • Manage presence and cursor tracking
  • Create comment threading infrastructure
  • Handle real-time updates and conflict resolution

By combining these powerful tools, you can build a collaborative application with minimal boilerplate code. Velt handles the real-time aspects, while Auth0 secures user authentication and management.

Initial Setup

Prerequisites

Ensure you have:

  • Node.js 16.8 or later
  • A Velt SDK account
  • An Auth0 account
  • Basic familiarity with Next.js and TypeScript

Project Initialization

Let's start by creating a new Next.js project with TypeScript and Tailwind CSS:

npx create-next-app@latest collaborative-editor --typescript --app
cd collaborative-editor
Enter fullscreen mode Exit fullscreen mode

Dependencies

Install the required packages:

npm install @auth0/nextjs-auth0 @veltdev/react react-quill
Enter fullscreen mode Exit fullscreen mode

These packages provide:

  • @auth0/nextjs-auth0: Auth0's SDK with Next.js-specific optimizations
  • @veltdev/react: Velt's React components and hooks
  • react-quill: A lightweight rich text editor (you can swap this with any other editor)

Auth0 Configuration

Auth0 is a powerful cloud-based platform that provides authentication and authorization services to help you simplify user management and provide secure login flows. With Auth0, you can easily integrate various authentication methods, including social logins and enterprise identity providers, into your application.

In this section, we'll set up Auth0 for our Next.js application to handle user authentication.

Environment Setup

Create .env.local with your Auth0 credentials:

# .env.local
AUTH0_SECRET='LONG_RANDOM_STRING'
APP_BASE_URL='http://localhost:3000'
AUTH0_DOMAIN='https://YOUR_DOMAIN.auth0.com'
AUTH0_CLIENT_ID='YOUR_CLIENT_ID'
AUTH0_CLIENT_SECRET='YOUR_CLIENT_SECRET'
Enter fullscreen mode Exit fullscreen mode

You can generate the Auth0 secret by running openssl rand -hex 32 in your terminal. This secret is used to securely sign the session cookies.

The APP_BASE_URL is the base URL of your application, which is used for callback URLs and redirects.

We will get the AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET from the Auth0 dashboard after creating an application.

Auth0 Application Setup

To set up Auth0, log in to your Auth0 dashboard, and on the side panel, select Applications > Applications, then click Create Application.

Image1

Give your application a name, choose Regular Web Application from the provided options, and click Create. Then select Next.js as the technology.

Image2

Next, select Settings and copy the following values into your .env.local:

  • Domain
  • Client ID
  • Client Secret

Make sure to set the Allowed Callback URLs to http://localhost:3000/auth/callback, and the Allowed Logout URLs to http://localhost:3000.

Image3

Auth0 SDK Integration

The Auth0 SDK offers a range of methods for handling user authentication, session management, and additional features. We'll create a wrapper around the Auth0 client to simplify its usage in our Next.js application.

Create lib/auth0.ts with the following content:

// lib/auth0.ts
import { Auth0Client } from "@auth0/nextjs-auth0/server";

export const auth0 = new Auth0Client();
Enter fullscreen mode Exit fullscreen mode

Authentication Middleware

Set up the middleware to handle auth routes and protect your application routes. Create a middleware.ts file in the root of your project:

// middleware.ts
import type { NextRequest } from "next/server";
import { auth0 } from "./lib/auth0";

export async function middleware(request: NextRequest) {
  return await auth0.middleware(request);
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
};
Enter fullscreen mode Exit fullscreen mode

The matcher pattern excludes static assets while protecting all other routes.

Home Page with Auth0

Since we are using Auth0 for authentication, we need to create a home page that allows users to log in or sign up. This page will display buttons for logging in and signing up, and it will redirect users to the appropriate Auth0 login flow.

Update file pages/index.tsx to handle the home page with login and signup buttons:

// pages/index.tsx
import Link from "next/link";
import React from "react";
import { useUser } from "@auth0/nextjs-auth0";

const Page = () => {
  const { user, isLoading } = useUser();

  if (!user && !isLoading) {
    return (
      <main className="min-h-screen flex flex-col items-center justify-center bg-gray-50">
        <div className="text-center space-y-8 p-8 max-w-md w-full bg-white rounded-lg shadow-lg">
          <h1 className="text-3xl font-bold text-gray-800">
            Collaborative Editor
          </h1>
          <p className="text-gray-600">
            A real-time collaborative editing platform. Work together with your
            team in real-time.
          </p>
          <div className="space-y-4">
            <Link href="/auth/login?screen_hint=signup" className="block">
              <button className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors">
                Sign up
              </button>
            </Link>
            <Link href="/auth/login" className="block">
              <button className="w-full border border-gray-300 hover:border-gray-400 text-gray-700 font-medium py-2 px-4 rounded-md transition-colors">
                Log in
              </button>
            </Link>
          </div>
        </div>
      </main>
    );
  }

  return (
    <main className="min-h-screen bg-gray-50">
      <div className="max-w-4xl mx-auto p-8">
        <div className="bg-white rounded-lg shadow-lg p-6">
          <div className="flex justify-between items-center mb-6">
            <h1 className="text-2xl font-bold text-gray-800">
              Welcome, {user?.name}!
            </h1>
            <div className="flex gap-4">
              <Link href="/editor">
                <button className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors">
                  Go to Editors
                </button>
              </Link>
              <Link href="/auth/logout">
                <button className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-md transition-colors">
                  Log out
                </button>
              </Link>
            </div>
          </div>
          <p className="text-gray-600">
            Start collaborating on your documents in real-time.
          </p>
        </div>
      </div>
    </main>
  );
};

export default Page;
Enter fullscreen mode Exit fullscreen mode

Running this code will give you a simple home page with options to log in or sign up. Once logged in, users will see a welcome message, a button to access the editors' page, and a logout button. The button actions are handled by Auth0's SDK, which will redirect users to the appropriate login or logout flows.

AuthProvider Component

To protect routes and ensure only authenticated users can access certain pages, create a reusable AuthProvider component:

// providers/AuthProvider.tsx
import { useUser } from "@auth0/nextjs-auth0";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function AuthProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const { user, isLoading } = useUser();
  const router = useRouter();

  useEffect(() => {
    if (!isLoading && !user) {
      router.push("/");
    }
  }, [user, isLoading, router]);

  if (isLoading)
    return (
      <div className="min-h-screen flex items-center justify-center bg-gray-50 text-gray-700">
        Loading...
      </div>
    );

  if (!user) return null;

  return <>{children}</>;
}

Enter fullscreen mode Exit fullscreen mode

Use this AuthProvider to wrap any page that requires authentication. For example, in your pages/editor.tsx, you can use it like this:

// pages/editor.tsx
import AuthProvider from "@/providers/AuthProvider";

const Page = () => {
  return (
    <AuthProvider>
      <h1>Editor&apos;s Page</h1>
    </AuthProvider>
  );
};

export default Page;
Enter fullscreen mode Exit fullscreen mode

The AuthProvider ensures that unauthenticated users get redirected to the home page where they can log in. You can test this by trying to access the /editor page without being logged in.

Velt Integration

Velt significantly reduces the complexity of building real-time collaborative features. Instead of managing WebSocket connections or building comment systems from scratch, Velt provides these features as composable React components, allowing for seamless integration.

Key benefits of using Velt include:

  • Scalable WebSocket infrastructure
  • Presence management and live cursor tracking
  • Thread-based commenting system
  • Automatic user state synchronization
  • Instant notifications for comments and mentions
  • Easy integration with React applications

To get started with Velt, follow these steps:

Velt Provider Setup

The VeltProvider component initializes the Velt client and manages the real-time connection state. It acts as the root component for all Velt features and functions.

Obtain your Velt API key from the Velt console under App Configuration.

Image4

Then in your Next.js app, create a dedicated provider component:

// providers/VeltReactProvider.tsx
import { VeltProvider } from "@veltdev/react";
import React from "react";

const VeltReactProvider = ({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) => {
  return (
    <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_API_KEY!}>
      {children}
    </VeltProvider>
  );
};

export default VeltReactProvider;
Enter fullscreen mode Exit fullscreen mode

This provider should wrap your application root to ensure Velt's features are available throughout your component tree. You can add it to your _app.tsx file:

// _app.tsx
import type { AppProps } from "next/app";
import VeltReactProvider from "@/providers/VeltReactProvider";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <div>
      <VeltReactProvider>
        <Component {...pageProps} />
      </VeltReactProvider>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Syncing Auth0 User with Velt

To ensure that Velt recognizes the authenticated user, you need to synchronize Auth0 user data with Velt using the useIdentify hook to create or update the user profile in Velt's backend.

When a user logs in through Auth0, you will receive their user information, including user ID, name, email, and photo url. You must pass this information to Velt to update the user's profile.

You can create a User object containing the relevant details. Then, when you call the identify() function with this User object, it will update the profile accordingly.

If it's the first time Velt has seen this user ID, Velt's backend will create a profile for this user. However, if the user ID already exists in Velt's backend, Velt will update the user's information if any details (such as name or photo URL) have changed.

To automate this process, let's create a custom React hook called useVeltAuth.

// hooks/useVeltAuth.ts
import { useUser } from "@auth0/nextjs-auth0";
import { useIdentify } from "@veltdev/react";

export default function useVeltAuth() {
  const { user: auth0User } = useUser();

  const veltUser = auth0User
    ? {
        userId: auth0User.sub || "",
        name: auth0User.name || "",
        email: auth0User.email || "",
        photoUrl: auth0User.picture || "",
        organizationId: "collaborative-editor",
        color: "#FF0000",
        textColor: "#FFFFFF",
      }
    : null;

  useIdentify(veltUser);

  if (!auth0User) return;
}
Enter fullscreen mode Exit fullscreen mode

You can then call useVeltAuth() on your home page, where we currently handle the user authentication.

// pages/index.tsx
import useVeltAuth from "@/hooks/useVeltAuth";

const Page = () => {
  useVeltAuth();
  // ...existing code...
};
Enter fullscreen mode Exit fullscreen mode

This approach ensures that every authenticated user is registered in Velt's backend, enabling robust collaboration features and user management.

Quill Editor Setup

To build a collaborative document editor, we need a rich text editor that supports real-time updates. For this, we will use React Quill, which is a React wrapper for the Quill rich text editor.

Building the Editor Component

Let's break down the main components that make up our collaborative editor page:

[h4] 1. UserDropdown Component

This component displays the current user's email and provides a dropdown menu for navigation and logout. It uses Auth0's useUser hook to access user information.

// components/UserDropdown.tsx
import React, { useState } from "react";
import Link from "next/link";
import { useUser } from "@auth0/nextjs-auth0";

const UserDropdown = () => {
  const { user } = useUser();

  const [open, setOpen] = useState(false);

  return (
    <div className="relative inline-block text-left">
      <button
        onClick={() => setOpen(v => !v)}
        className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md shadow-sm text-gray-700 font-medium">
        <span className="font-semibold">{user?.name || user?.email}</span>
        <svg
          className="w-4 h-4"
          fill="none"
          stroke="currentColor"
          strokeWidth={2}
          viewBox="0 0 24 24">
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            d="M19 9l-7 7-7-7"
          />
        </svg>
      </button>
      {open && (
        <div className="absolute right-0 mt-2 w-40 bg-white border border-gray-200 rounded-md shadow-lg z-10">
          <Link
            href="/"
            className="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">
            Home
          </Link>
          <Link href="/auth/logout" className="block">
            <button className="w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">
              Log out
            </button>
          </Link>
        </div>
      )}
    </div>
  );
};

export default UserDropdown;
Enter fullscreen mode Exit fullscreen mode

[h4] 2. RichTextEditor Component

This component provides a rich text editor (using React Quill) and a live preview. We will integrate it with Velt later on for real-time collaboration.

// components/RichTextEditor.tsx
import React, { useState } from "react";
import dynamic from "next/dynamic";

const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
import "react-quill/dist/quill.snow.css";

const RichTextEditor = () => {
  const [value, setValue] = useState("");

  return (
    <div className="flex flex-col md:flex-row gap-8 mt-8 h-[calc(100vh-8rem)]">
      <section className="flex-1 md:w-[50%] bg-white rounded-lg shadow-lg p-6 flex flex-col">
        <h2 className="text-lg font-semibold mb-4 text-gray-700">Editor</h2>
        <div className="flex-1 max-h-full overflow-auto">
          <ReactQuill
            theme="snow"
            value={value}
            onChange={setValue}
            style={{ height: "100%" }}
          />
        </div>
      </section>
      <section className="flex-1  md:w-[50%] bg-white rounded-lg shadow-lg p-6 flex flex-col">
        <h2 className="text-lg font-semibold mb-4 text-gray-700">Preview</h2>
        <div className="flex-1 min-h-0 max-h-full overflow-auto max-w-none border-2 p-4 rounded-sm border-gray-200">
          <div
            id="editor-output"
            className="editor-output"
            dangerouslySetInnerHTML={{ __html: value }}
          />
        </div>
      </section>
    </div>
  );
};

export default RichTextEditor;
Enter fullscreen mode Exit fullscreen mode

[h4] 3. Editor Page

The editor page brings everything together. It protects the route with AuthProvider, displays the user dropdown, and renders the rich text editor.

// pages/editor.tsx
import React from "react";
import AuthProvider from "@/providers/AuthProvider";
import RichTextEditor from "@/components/RichTextEditor";
import UserDropdown from "@/components/UserDropdown";

const Page = () => {
  return (
    <AuthProvider>
      <main className="min-h-screen bg-gray-50 py-10">
        <div className="container mx-auto px-4">
          <div className="flex justify-between items-center mb-6">
            <h1 className="text-3xl font-extrabold text-blue-700 drop-shadow">
              Shared Document Editor
            </h1>
            <UserDropdown />
          </div>
          <RichTextEditor />
        </div>
      </main>
    </AuthProvider>
  );
};
export default Page;
Enter fullscreen mode Exit fullscreen mode

With these components, you have a modular, collaborative editor interface ready for real-time features and user management.

Adding Real-Time Collaboration with Velt SDK

Now that we have a basic editor set up, let's integrate real-time collaborative features from Velt, such as live comments and presence tracking.

Set up the document ID for comments

A document in Velt is a shared space where users can collaborate in real time. To enable comments and real-time updates, we need to define a unique document ID. Velt will use this ID to track changes and comments for this specific document.

Let’s use the useVeltClient() api to set the document ID to "collaborative-post":

// components/RichTextEditor.tsx
import { useVeltClient } from "@veltdev/react";

const { client } = useVeltClient();

useEffect(() => {
  if (client) {
    client.setDocument("collaborative-post", {
      documentName: "Welcome to the Collaborative Editor",
    });
  }
}, [client]);

const commentElement = client.getCommentElement();
commentElement.allowedElementIds(["editor-output"]);
Enter fullscreen mode Exit fullscreen mode

We also notified Velt to only allow comments on the editor output section by setting the allowed element IDs to ["editor-output"].

Adding Comments, Presence, and Notifications

Velt provides built-in support for comments, user presence tracking, and notifications. It provides you with different components to handle these features easily. For our editor, we will be using the following components:

  • VeltCommentTool: This feature enables users to add comments to selected text.
  • VeltComments: This displays all comments on the document.
  • VeltPresence: This shows the avatars for users currently online and collaborating on the document.
  • VeltSidebarButton: This button toggles the comments sidebar.
  • VeltCommentsSidebar: This displays the comments sidebar, showing all comments and allowing users to reply to them.

- VeltNotificationsTool: This displays real-time notifications for comments and mentions.

It is essential to note that you must enable notifications in your Velt console for VeltNotificationsTool to function properly. You can do this by going to the "In-app Notifications" section in your Velt dashboard and then enabling notifications for your application. Feel free to customize the settings to your liking.

Image5

Now, let's integrate these components into our editor page:

// pages/editor.tsx
import {
  VeltNotificationsTool,
  VeltCommentTool,
  VeltComments,
  VeltPresence,
  VeltSidebarButton,
  VeltCommentsSidebar,
} from "@veltdev/react";

const Page = () => {
  return (
    <AuthProvider>
      <main className="min-h-screen bg-gray-50 py-10">
        <div className="container mx-auto px-4">
          <div className="flex flex-col gap-y-3 gap-x-6 md:flex-row justify-between items-start mb-6">
            <div>
              <h1 className="text-3xl font-extrabold text-blue-700 drop-shadow">
                Shared Document Editor
              </h1>
              <div className="flex items-center justify-start mb-4">
                <p className="mr-2 font-semibold text-gray-600">Viewers:</p>
                <VeltPresence />
              </div>
            </div>
            <div className="flex justify-end items-center space-x-4">
              <VeltCommentTool />
              <VeltNotificationsTool />
              <UserDropdown />
            </div>
          </div>
          <RichTextEditor />
        </div>

        <div className="fixed bottom-10 right-10 z-50 space-y-4">
          <VeltSidebarButton />
          <VeltCommentsSidebar pageMode={true} />
          <VeltComments />
        </div>
      </main>
    </AuthProvider>
  );
};
export default Page;
Enter fullscreen mode Exit fullscreen mode

From the code snippet above, we see how easy it is to integrate the Velt SDK into your applications. You simply need to import the Velt components you wish to use and position them appropriately on your page.

Testing the Collaborative Editor

To test the features we just implemented, lets go to the /editor page and ensure everything is working as expected.

  1. Start your Next.js app:

    npm run dev
    
  2. Open your browser and navigate to http://localhost:3000.

  3. Open two separate windows and log in with different Auth0 accounts.

  4. Navigate to the editor's page. You should now see:

    • Both logged in user's avatar icons in the top left corner of the page
    • The rich text editor where both users can type and make changes to the current document
    • The comments tool icon at the top right corner that allows users to select text and add comments
    • The notifications tool at the top right corner that shows real-time notifications for comments and mentions
    • The comment sidebar button in the bottom right that, when clicked, displays all comments in the document and allows users to reply to them
    • The UserDropdown button we created earlier
  5. Try selecting text and adding comments. You should see the comments appear in real time for both users.

  6. Check the notifications tool to see if you receive notifications for comments and mentions.

Because Velt handles the real-time updates, presence, and comments, you don't need to worry about implementing these features from scratch. Velt has done the heavy lifting, allowing you to focus on building the core of your product.

Next Steps

Now that you have a basic collaborative editor with real-time features, you can consider the following enhancements:

  • Backend Sync: Currently, when one user makes changes to the document, those changes are not persisted or shared with other users. You can implement a backend sync mechanism to save the document state to a database.
  • Advanced Text Editor: We are currently using a basic rich text editor. You can enhance it with additional features, such as image uploads, formatting options, and more.
  • Markdown Support: If you want to support Markdown or other text formats, you can integrate a library that converts between rich text and Markdown.
  • Follow Mode: Implement a follow mode using Velt’s “Follow Me Mode” where one user can follow another user's cursor in real-time, similar to how Google Docs works.
  • Document History: Implement a version history feature that allows users to view and revert to previous versions of the document.
  • User Roles and Permissions: Set up access control on Velt to manage who can edit, comment, or view the document.

Conclusion

In this tutorial, we built a real-time collaborative document editor using Next.js, Auth0, and Velt. We covered how to set up authentication with Auth0, create a rich text editor using Quill, and integrate Velt SDK for real-time collaboration, with feature like comments, mentions, and presence.

By leveraging Velt's infrastructure, we avoided the complexities of building real-time collaboration features from scratch, allowing us to focus on creating a great user experience.

This setup provides a solid foundation for building collaborative applications and you can further extend it based on your specific requirements. Whether you're building a simple note-taking app or a complex document editor, Velt and Auth0 make it easier to implement real-time collaboration features without having to reinvent the wheel.

You can find the full codebase on GitHub to get started quickly. Feel free to customize the editor, add more features, and make it your own!

Resources

Top comments (1)

Collapse
 
astrodevil profile image
Astrodevil

Good tutorial