Sep 18, 2025 · 14 min read

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:
The final application will enable authenticated users to:
By using Velt you are able to focus on building your application without worrying about the complexities of real-time collaboration.
Next.js + Auth0 provides:
Velt SDK eliminates the need to:
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.
Ensure you have:
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
Install the required packages:
npm install @auth0/nextjs-auth0 @veltdev/react react-quill
These packages provide:
@auth0/nextjs-auth0: Auth0's SDK with Next.js-specific optimizations@veltdev/react: Velt's React components and hooksreact-quill: A lightweight rich text editor (you can swap this with any other editor)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.
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'
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.
To set up Auth0, log in to your Auth0 dashboard, and on the side panel, select Applications > Applications, then click Create Application.
Give your application a name, choose Regular Web Application from the provided options, and click Create. Then select Next.js as the technology.
Next, select Settings and copy the following values into your .env.local:
Make sure to set the Allowed Callback URLs to http://localhost:3000/auth/callback, and the Allowed Logout URLs to http://localhost:3000.
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();
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).*)",
],
};
The matcher pattern excludes static assets while protecting all other routes.
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;
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.
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}</>;
}
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's Page</h1>
</AuthProvider>
);
};
export default Page;
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 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:
To get started with Velt, follow these steps:
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.
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;
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>
);
}
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;
}
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...
};
This approach ensures that every authenticated user is registered in Velt's backend, enabling robust collaboration features and user management.
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.
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;
[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;
[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;
With these components, you have a modular, collaborative editor interface ready for real-time features and user management.
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.
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"]);
We also notified Velt to only allow comments on the editor output section by setting the allowed element IDs to ["editor-output"].
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:
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.
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;
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.
To test the features we just implemented, lets go to the /editor page and ensure everything is working as expected.
Start your Next.js app:
npm run dev
Open your browser and navigate to http://localhost:3000.
Open two separate windows and log in with different Auth0 accounts.
Navigate to the editor's page. You should now see:
Try selecting text and adding comments. You should see the comments appear in real time for both users.
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.
Now that you have a basic collaborative editor with real-time features, you can consider the following enhancements:
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!
Also published on DEV.to.