DEV Community

Cover image for Build Real-Time Presence Features Like Figma and Google Docs in Your App in Minutes๐Ÿš€๐Ÿ”ฅ๐Ÿง‘โ€๐Ÿ’ป
Astrodevil
Astrodevil

Posted on

Build Real-Time Presence Features Like Figma and Google Docs in Your App in Minutes๐Ÿš€๐Ÿ”ฅ๐Ÿง‘โ€๐Ÿ’ป

TL;DR

Learn about the challenges and solutions for implementing collaborative features in your app. We built a real-time "Who's Online?" wall using Velt inside this tutorial.

Features include:

  • Real-time display of online users and their cursors.
  • User authentication and document context management.
  • A customizable UI with commenting and sign-out options.

This guide lays the foundation for creating engaging, collaborative tools. To enhance it further, consider adding features like reactions, notifications, and secure sign-in methods.

Letโ€™s start ๐Ÿš€!


Modern web applications are increasingly collaborative. Think about how natural it feels to see those colorful cursors moving around in Figma, or those profile bubbles in Google Docs showing who's viewing the document. These presence features have become essential for any collaborative app.

Did you know that 97% of users are more likely to stay engaged with a product when they can see other users actively collaborating in real-time? The psychology behind this is fascinating - we're naturally drawn to spaces where we can see others working alongside us, even in digital environments.

Users collaborating in real-time

Challenges of Building a Presence Feature from Scratch

Building presence features seems straightforward at first - just track who's online, right? But as many developers discover, it quickly becomes complex. Take a recent project I worked on: we started with a simple websocket connection to show active users, but things got messy when we needed to handle unstable connections and browser tabs.

Let's look at the backend first. You'll need a robust system to maintain websocket connections across multiple server instances. Here's a common pattern using Redis:

// Server-side presence tracking
const presence = new Map();
redis.subscribe('presence', (channel, message) => {
   const { userId, status } = JSON.parse(message);
   presence.set(userId, status);
   broadcastPresence();
});
Enter fullscreen mode Exit fullscreen mode

The frontend brings its own challenges. Ever noticed how Google Docs shows you as "away" when you switch tabs? Implementing accurate presence states means handling various browser events:

// Frontend presence detection
window.addEventListener('visibilitychange', () => {
    if (document.hidden) {
        updatePresence('away');
    } else {
        updatePresence('active');
    }
});
Enter fullscreen mode Exit fullscreen mode

User state transitions diagram

One of the trickiest parts is distinguishing between a user who's truly offline versus temporarily disconnected. You might think a simple timeout would work:

// WARNING: Oversimplified approach
socket.on('disconnect', () => {
    setTimeout(() => markUserOffline(userId), 30000);
});
Enter fullscreen mode Exit fullscreen mode

But real-world connections are messier. Users might have poor connectivity, their laptop might go to sleep, or they might just close their laptop without properly disconnecting. A more robust solution needs heartbeat mechanisms and cleanup functions to handle these edge cases.

Network connectivity scenarios

For companies operating globally, multi-region support adds another layer of complexity. Your presence system needs to sync user states across different geographic regions with minimal latency. This often involves setting up presence servers in each region and implementing complex state reconciliation:

// Multi-region presence sync
function syncPresenceAcrossRegions(userId, status) {
    const regions = ['us-east', 'eu-west', 'ap-south'];
    regions.forEach(region => {
        if (region !== currentRegion) {
            notifyRegion(region, userId, status);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

The good news? You don't have to build all of this from scratch anymore. Modern solutions handle these edge cases while giving you the flexibility to customize the experience for your users. Whether you're building the next Figma or adding basic collaboration to your app, understanding these challenges helps you make better architectural decisions.

Why SDKs Are becoming popular

Developers are turning to SDKs to build presence features because it's simply more practical than building everything from scratch. Here's why:

1. Time savings - Instead of spending weeks handling edge cases like network disconnections or browser tabs, you can integrate presence in a few hours.

2. Proven solutions - Popular SDKs have already solved common problems like:

  • Managing flaky internet connections
  • Handling multiple browser tabs
  • Cleaning up when users leave
  • Syncing presence across servers

3. Cost effective - The time and resources needed to build and maintain a custom presence system often costs more than using an SDK.

Real-world examples where SDKs make sense:

  • Document editors showing who's viewing or editing
  • Chat applications displaying online/offline status
  • Design tools showing where other users are working
  • Meeting platforms indicating who's currently speaking
  • Data analytics platforms showing real-time collaborators on dashboards
  • Video editing software displaying who's working on which timeline segments

The choice between building custom or using an SDK comes down to your specific needs. If you need basic presence features that just work, an SDK is usually the way to go. Custom solutions make sense mainly for unique requirements or when you need complete control over the implementation.

One of the most popular SDKs for building presence features is Velt. At its core, Velt provides a full suite of collaboration features found in popular apps like Figma, Google Docs etc. They handle the complex infrastructure needed for live collaboration features. It manages the WebSocket connections, state synchronization, and presence tracking across users and sessions.

What makes it particularly useful for developers is that it abstracts away the typical headaches of building real-time features - things like handling connection drops, managing presence across multiple tabs, and cleaning up stale sessions. The SDK provides ready-to-use components for common presence patterns while still allowing low-level access to the presence data when needed.


Setting up Velt in your project and adding presence

Let's build a real-time "Who's Online?" wall that shows active users on your website. We'll use Next.js 15 with TypeScript and Velt's presence features.

Here's how it's going to look:

Project Setup

First, create a new Next.js project with TypeScript:

npx create-next-app@latest whos-online --typescript --tailwind --app
cd whos-online
Enter fullscreen mode Exit fullscreen mode

Install the Velt SDK:

npm install @veltdev/react
npm install --save-dev @veltdev/types
Enter fullscreen mode Exit fullscreen mode

Configuration

Head over to the Velt Console and get your Velt API key. This will be used to authenticate your requests to the Velt API.

Then store this API key in your .env file:

NEXT_PUBLIC_VELT_API_KEY=your_api_key
Enter fullscreen mode Exit fullscreen mode

Create a new provider component to initialize Velt:

'use client'

import { VeltProvider as BaseVeltProvider } from "@veltdev/react"

export function VeltProvider({ children }: { children: React.ReactNode }) {
  return (
    <BaseVeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_API_KEY!}>
      {children}
    </BaseVeltProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Next, In your root component, wrap your app in the VeltProvider component:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { VeltProvider } from "./provider/VeltProvider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = { // Next.js metadata
  title: "Who's Online?",
  description: "A real-time presence feature built with Velt",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <VeltProvider> // VeltProvider is a component that provides the Velt context to the app
      <html lang="en">
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          {children}
        </body>
      </html>
    </VeltProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Authenticating Users

First, let's create a type for our user data:

export interface UserData { // UserData is an interface that defines the structure of the user data
  userId: string;
  name: string;
  email: string;
  photoUrl?: string;
  color: string;
  textColor: string;
}
Enter fullscreen mode Exit fullscreen mode

Now, we need a way to let Velt know who the user is. There is a useVeltClient hook that you can use to identify the user. It works in the following way:

import { useVeltClient } from '@veltdev/react';

const { client } = useVeltClient();

// Perform authentication

client.identify(user); // here user is the user data that you want to identify the user with
Enter fullscreen mode Exit fullscreen mode

Next, we also need to set the document context. This is the document that the user will be interacting with.

const { client } = useVeltClient();

useEffect(() => {
    if (client) {
        client.setDocument('unique-document-id', {documentName: 'Document Name'});
    }
}, [client]);
Enter fullscreen mode Exit fullscreen mode

The client.setDocument method takes two arguments:

  • The first argument is the documentId. This is a unique identifier for the document that you want to set the context for.
  • The second argument is an object that contains the document metadata. This is a key-value pair object that can be used to store any metadata about the document.

In our simple "Who's Online?" app, we will ask the user to enter their name and email and then identify the user with Velt.

'use client'

import { useState, useEffect } from 'react';
import { useVeltClient } from '@veltdev/react';
import { UserData } from '../types';
import { User } from '@veltdev/types';

const generateRandomColor = () => { // generateRandomColor is a function that generates a random color
  const hue = Math.floor(Math.random() * 360);
  const pastelSaturation = 70;
  const pastelLightness = 80;
  return `hsl(${hue}, ${pastelSaturation}%, ${pastelLightness}%)`;
};

const getContrastColor = (backgroundColor: string) => { // getContrastColor is a function that returns a contrasting color for the given background color
  const hsl = backgroundColor.match(/\d+/g)?.map(Number);
  if (!hsl) return '#000000';

  const lightness = hsl[2];
  return lightness > 70 ? '#000000' : '#ffffff';
};

export function UserAuth() { // UserAuth is a component that allows the user to authenticate
  const { client } = useVeltClient();
  const [isAuthenticated, setIsAuthenticated] = useState(() => { // state to check if the user is authenticated
    return !!localStorage.getItem('userData');
  });

  useEffect(() => {
    const initializeUser = async () => { // function to set the user data
      const savedUser = localStorage.getItem('userData');
      if (savedUser && client) {
        const userData: UserData = JSON.parse(savedUser);
        await client.identify(userData as User);
        client.setDocument('whos-online-wall', {documentName: 'Who\'s Online?'});
      }
    };

    if (client) {
      initializeUser();
    }
  }, [client]);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const name = formData.get('name') as string;
    const email = formData.get('email') as string;

    if (!name || !email || !client) return;

    const backgroundColor = generateRandomColor();
    const userData: UserData = {
      userId: email,
      name,
      email,
      color: backgroundColor,
      textColor: getContrastColor(backgroundColor)
    };

    localStorage.setItem('userData', JSON.stringify(userData));
    await client.identify(userData as User);
    client.setDocument('whos-online-wall', {documentName: 'Who\'s Online?'});
    setIsAuthenticated(true);
  };

  if (isAuthenticated) return null; // if the user is authenticated, return null

  return ( // actual component
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
      <div className="bg-white dark:bg-gray-800 p-8 rounded-xl shadow-2xl max-w-md w-full mx-4 border border-gray-100 dark:border-gray-700">
        <h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white text-center">
          Welcome! ๐Ÿ‘‹
          <span className="block text-lg font-normal mt-2 text-gray-600 dark:text-gray-300">
            Please introduce yourself
          </span>
        </h2>
        <form onSubmit={handleSubmit} className="space-y-6">
          <div>
            <label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
              Name
            </label>
            <input
              type="text"
              name="name"
              id="name"
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-600 
                       px-4 py-3 bg-white dark:bg-gray-700 text-gray-900 dark:text-white
                       focus:ring-2 focus:ring-blue-500 focus:border-transparent
                       transition-colors duration-200"
              placeholder="Enter your name"
            />
          </div>
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
              Email
            </label>
            <input
              type="email"
              name="email"
              id="email"
              required
              className="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-600 
                       px-4 py-3 bg-white dark:bg-gray-700 text-gray-900 dark:text-white
                       focus:ring-2 focus:ring-blue-500 focus:border-transparent
                       transition-colors duration-200"
              placeholder="Enter your email"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 text-white rounded-lg px-4 py-3 
                     hover:bg-blue-700 transform hover:scale-[1.02]
                     transition-all duration-200 font-medium text-base
                     focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
                     shadow-lg hover:shadow-xl"
          >
            Join Now
          </button>
        </form>
      </div>
    </div>
  );
} 
Enter fullscreen mode Exit fullscreen mode

Here, we're expecting a name and email from the user and then we're storing this data in local storage. At the same time, we're identifying the user with Velt using the client.identify method.

Then we're setting the document context using the client.setDocument method. You can learn more about authentication here and setting the document context here.

Let's see how this works in the browser:

UserAuth

Adding the Online Wall

Now that we have the user authenticated and the document context set, we can add the online wall to our app.

To do this, we'll use the VeltPresence component. This component will automatically handle the presence tracking for us.

'use client'

import { useVeltClient, VeltPresence } from '@veltdev/react'; // import the VeltPresence component

export function OnlineWall() { // OnlineWall is a component that shows the online wall
  const { client } = useVeltClient();

  if (!client) return null; // if the client is not initialized, return null

  return (
    <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-4">
      <VeltPresence />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now after joining the wall, you should see the online wall with the users who have joined the wall.

OnlineWall

I joined from three different windows and you can see the users who have joined the wall. Make sure to join via two different browser profiles or on incognito tab to prevent conflict between the users.

But what if I wanted to add a custom UI to the wall? What if I wanted to allow users to leave the wall?

Let's see how we can do that.

Customizing the Online Wall and allowing users to leave

We can customize the online wall UI by using the usePresenceUsers hook. This hook returns the list of users who are currently online. Then we can use this list to render the users in our own custom UI.

To allow users to leave the wall, we can use the client.signOutUser method.

Here's the logoutButton for the wall:

'use client'

import { useVeltClient } from '@veltdev/react';

export function LogoutButton() {
  const { client } = useVeltClient();

  const handleLogout = async () => {
    if (client) {
      await client.signOutUser(); // this will sign out the user from the current document
      localStorage.removeItem('userData'); // this will remove the user data from local storage
      window.location.reload(); // this will reload the page
    }
  };

  const isAuthenticated = !!localStorage.getItem('userData');
  if (!isAuthenticated) return null;

  return (
    <button
      onClick={handleLogout}
      className="fixed top-4 right-4 px-4 py-2 bg-gray-100 dark:bg-gray-700 
                 text-gray-700 dark:text-gray-200 rounded-lg hover:bg-gray-200 
                 dark:hover:bg-gray-600 transition-colors duration-200 
                 flex items-center gap-2 shadow-sm"
    >
      <span>Logout</span>
      <svg 
        xmlns="http://www.w3.org/2000/svg" 
        width="16" 
        height="16" 
        viewBox="0 0 24 24" 
        fill="none" 
        stroke="currentColor" 
        strokeWidth="2" 
        strokeLinecap="round" 
        strokeLinejoin="round"
      >
        <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
        <polyline points="16 17 21 12 16 7" />
        <line x1="21" y1="12" x2="9" y2="12" />
      </svg>
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's now update the OnlineWall component to use the usePresenceUsers hook and update the UI to show the users in a nice grid.

'use client'

import { useVeltClient, usePresenceUsers } from '@veltdev/react';
import { motion } from 'framer-motion';

export function OnlineWall() {
  const { client } = useVeltClient();
  const presenceUsers = usePresenceUsers();

  if (!client) return null;

  // Add loading state check
  if (!presenceUsers) {
    return (
      <div className="flex items-center justify-center min-h-[200px]">
        <div className="relative">
          {/* Outer spinning ring with gradient */}
          <div className="w-16 h-16 rounded-full absolute animate-spin 
            bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500"
          ></div>
          {/* Inner white circle */}
          <div className="w-16 h-16 rounded-full absolute bg-background"></div>
          {/* Middle spinning ring with gradient */}
          <div className="w-12 h-12 rounded-full absolute top-2 left-2 animate-spin 
            bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500"
          ></div>
          {/* Inner white circle */}
          <div className="w-12 h-12 rounded-full absolute top-2 left-2 bg-background"></div>
          {/* Center dot with pulse effect */}
          <div className="w-8 h-8 rounded-full absolute top-4 left-4
            bg-gradient-to-r from-blue-500 to-purple-500 animate-pulse"
          ></div>
        </div>
      </div>
    );
  }

  // Get current user data from localStorage
  const currentUserData = localStorage.getItem('userData');
  const currentUser = currentUserData ? JSON.parse(currentUserData) : null;

  // Sort users to put current user first
  const sortedUsers = presenceUsers?.sort((a, b) => {
    if (a.userId === currentUser?.userId) return -1;
    if (b.userId === currentUser?.userId) return 1;
    return 0;
  });

  return ( // actual component
    <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-4">
      {sortedUsers?.map((user) => {
        const isCurrentUser = user.userId === currentUser?.userId;

        return (
          <motion.div
            key={user.userId}
            initial={{ opacity: 0, scale: 0.9 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.9 }}
            whileHover={{ scale: 1.05 }}
            className="relative group"
          >
            {isCurrentUser && (
              <div className="absolute -top-2 -right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-10 shadow-lg">
                You
              </div>
            )}
            <div 
              className={`rounded-lg p-4 h-full shadow-lg transition-all duration-300 group-hover:shadow-xl
                ${isCurrentUser ? 'ring-2 ring-blue-500 ring-offset-2 ring-offset-background' : ''}`}
              style={{ 
                backgroundColor: user.color || '#f0f0f0',
                color: user.textColor || '#000000'
              }}
            >
              <div className="flex items-center space-x-3">
                {user.photoUrl ? (
                  <img 
                    src={user.photoUrl} 
                    alt={user.name}
                    className="w-12 h-12 rounded-full border-2 border-white/30"
                  />
                ) : (
                  <div 
                    className="w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold border-2 border-white/30"
                    style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
                  >
                    {user.name?.charAt(0).toUpperCase()}
                  </div>
                )}
                <div className="flex-1 min-w-0">
                  <h3 className="font-semibold truncate">{user.name}</h3>
                  <p className="text-sm opacity-75 truncate">{user.email}</p>
                </div>
              </div>

              <div className="mt-3 flex items-center">
                <motion.div
                  className="w-2 h-2 rounded-full bg-green-400 mr-2"
                  animate={{
                    scale: [1, 1.2, 1],
                  }}
                  transition={{
                    duration: 2,
                    repeat: Infinity,
                  }}
                />
                <span className="text-sm">Online now</span>
              </div>
            </div>
          </motion.div>
        );
      })}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here we show the user's in a nice grid and we also indicate the current user with a "You" badge.

Here I am also showing a loading state when the presence users are being fetched. This can be done using the usePresenceUsers hook and checking if the users are null.

Here's how it looks:

Enhanced Online Wall

Logging out of the wall is as simple as clicking the logout button.

How does it look?

Awesome GIF

How easy was that? With just a few lines of code, we've added a presence feature to our app.

Showing cursors in the online wall

Let's take a step further and show the cursors of the users in the online wall. This means that whatever the user is doing in the document, we'll show the cursor of the user in the online wall.

To do this, we'll use the VeltCursor component. This component will automatically handle the cursor tracking for us. It's as simple as adding the component to the root.

What's particularly powerful about Velt's cursor implementation is that it doesn't just track raw x,y coordinates - it intelligently adapts to the underlying content structure. This means that even if users have different screen sizes, zoom levels, or different layouts (like responsive design changes), the cursors will always appear in the correct position relative to the content they're interacting with. This semantic understanding of the document structure ensures consistent cursor positioning across all clients.

Here the root is the OnlineWall component. It can be handled better in a separate root component but let's keep it simple for now.

    <>
      <VeltCursor /> // just add this to the root 
      <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-4">
        {sortedUsers?.map((user) => {
          const isCurrentUser = user.userId === currentUser?.userId;

          return (
            <motion.div
              key={user.userId}
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.9 }}
              whileHover={{ scale: 1.05 }}
              className="relative group"
            >
              {isCurrentUser && (
                <div className="absolute -top-2 -right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-10 shadow-lg">
                  You
                </div>
              )}
              <div 
                className={`rounded-lg p-4 h-full shadow-lg transition-all duration-300 group-hover:shadow-xl
                  ${isCurrentUser ? 'ring-2 ring-blue-500 ring-offset-2 ring-offset-background' : ''}`}
                style={{ 
                  backgroundColor: user.color || '#f0f0f0',
                  color: user.textColor || '#000000'
                }}
              >
                <div className="flex items-center space-x-3">
                  {user.photoUrl ? (
                    <img 
                      src={user.photoUrl} 
                      alt={user.name}
                      className="w-12 h-12 rounded-full border-2 border-white/30"
                    />
                  ) : (
                    <div 
                      className="w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold border-2 border-white/30"
                      style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
                    >
                      {user.name?.charAt(0).toUpperCase()}
                    </div>
                  )}
                  <div className="flex-1 min-w-0">
                    <h3 className="font-semibold truncate">{user.name}</h3>
                    <p className="text-sm opacity-75 truncate">{user.email}</p>
                  </div>
                </div>

                <div className="mt-3 flex items-center">
                  <motion.div
                    className="w-2 h-2 rounded-full bg-green-400 mr-2"
                    animate={{
                      scale: [1, 1.2, 1],
                    }}
                    transition={{
                      duration: 2,
                      repeat: Infinity,
                    }}
                  />
                  <span className="text-sm">Online now</span>
                </div>
              </div>
            </motion.div>
          );
        })}
      </div>
    </>
Enter fullscreen mode Exit fullscreen mode

We're wrapping the component in a fragment because we're adding the VeltCursor component to the root and this allows to add more components to the root.

This can be maintained in a better way but this is a quick and dirty solution.

Adding comments to our online wall

What if we wanted to make the wall more interactive and allow users to leave comments anywhere on the wall? This becomes really simple with Velt.

We can use the VeltComments and the VeltCommentTool components to allow users to leave comments anywhere on the wall.

Here's how it works:

    <>
      <VeltCursor />
      <VeltComments />
      <div className="fixed bottom-4 right-4 z-50">
        <VeltCommentTool />
      </div>
      <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 p-4">
        {sortedUsers?.map((user) => {
          const isCurrentUser = user.userId === currentUser?.userId;

          return (
            <motion.div
              key={user.userId}
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.9 }}
              whileHover={{ scale: 1.05 }}
              className="relative group"
            >
              {isCurrentUser && (
                <div className="absolute -top-2 -right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-full z-10 shadow-lg">
                  You
                </div>
              )}
              <div 
                className={`rounded-lg p-4 h-full shadow-lg transition-all duration-300 group-hover:shadow-xl
                  ${isCurrentUser ? 'ring-2 ring-blue-500 ring-offset-2 ring-offset-background' : ''}`}
                style={{ 
                  backgroundColor: user.color || '#f0f0f0',
                  color: user.textColor || '#000000'
                }}
              >
                <div className="flex items-center space-x-3">
                  {user.photoUrl ? (
                    <img 
                      src={user.photoUrl} 
                      alt={user.name}
                      className="w-12 h-12 rounded-full border-2 border-white/30"
                    />
                  ) : (
                    <div 
                      className="w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold border-2 border-white/30"
                      style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
                    >
                      {user.name?.charAt(0).toUpperCase()}
                    </div>
                  )}
                  <div className="flex-1 min-w-0">
                    <h3 className="font-semibold truncate">{user.name}</h3>
                    <p className="text-sm opacity-75 truncate">{user.email}</p>
                  </div>
                </div>

                <div className="mt-3 flex items-center">
                  <motion.div
                    className="w-2 h-2 rounded-full bg-green-400 mr-2"
                    animate={{
                      scale: [1, 1.2, 1],
                    }}
                    transition={{
                      duration: 2,
                      repeat: Infinity,
                    }}
                  />
                  <span className="text-sm">Online now</span>
                </div>
              </div>
            </motion.div>
          );
        })}
      </div>
    </>
Enter fullscreen mode Exit fullscreen mode

Let's see how the final app looks like:

It's so simple! Now our wall is more interactive and engaging.

Summary of the Project

In this project, we've built a real-time "Who's Online?" wall using Velt. We've learned how to:

  • Set up Velt in our project
  • Authenticate users with Velt
  • Set the document context for the current user
  • Use the VeltPresence component to show the online users
  • Customize the online wall UI using the usePresenceUsers hook
  • Allow users to leave the wall and sign out
  • Show cursors of the users in the online wall
  • Allow users to leave comments anywhere on the wall

Benefits of using Velt

Building presence features from scratch often starts as a fun weekend project. You set up WebSocket connections, track user states, and get a basic version working locally. Then reality hits - handling disconnections, syncing across regions, and managing browser quirks turns that weekend project into weeks of debugging.

Velt handles these complexities while still giving you the control you need. Instead of wrestling with WebSocket reconnection logic or debouncing presence updates, you can focus on crafting the actual user experience. It's like using a battle-tested authentication system instead of rolling your own crypto - sure, you could build it, but why would you?

The real value shows up when your app grows. When that small team collaboration tool suddenly needs to handle hundreds of concurrent users, or when your US-only app needs to expand globally, you don't need to rewrite your presence infrastructure. The same SDK that handled 10 users will scale to handle thousands.

Velt

Some practical benefits developers appreciate:

  • No need to maintain WebSocket servers and connection logic
  • Built-in handling for network issues and browser tab synchronization
  • Simple integration with existing authentication systems
  • Automatic cleanup of stale presence data

Think of it like using Redis for caching - could you build your own caching system? Absolutely. But Redis gives you a proven solution that just works, letting you solve your actual business problems instead.

The best part is that you're not locked into a specific implementation. Want to show presence differently in different parts of your app? Need to add custom presence states? The SDK gives you the building blocks while letting you control how everything looks and behaves.

Additional Velt Features

Beyond presence and real-time cursors, Velt offers several powerful collaboration features:

  1. Live Reactions - Add floating emojis and reactions that appear in real-time

  2. Follow Mode - Let users follow each other's movements and actions. Perfect for presentations and guided tutorials

  3. Huddles - Create instant audio/video meeting spaces within your app. Seamless integration with your existing UI

  4. Live Selection - Select and highlight text in real-time. Perfect for collaborative editing

  5. Video Player Sync - Sync video players across users. Perfect for video conferencing and presentations

Check more features here

Conclusion

In this article, we've seen how easy it is to add a presence feature to your app using Velt. We've learned how to set up Velt, authenticate users, set the document context, and use the VeltPresence component to show the online users. We've also seen how to customize the online wall UI using the usePresenceUsers hook and allow users to leave the wall and sign out.

You can further enhance our online wall by adding more features like real-time reactions, follow me mode, huddles, notifications and so much more!

In our wall, we did not implement a secure way to sign in users. We'll leave that as an exercise for you to implement. You can use any social login provider or email/password authentication and configure the Velt auth provider to use it.

I hope this article has given you a good understanding of how to build a presence feature into your app using Velt.

Happy coding!


To explore more about Velt and get started, check out:

we

Top comments (8)

Collapse
 
arindam_1729 profile image
Arindam Majumder

Amazing Article ๐Ÿ™Œ๐Ÿผ

Collapse
 
astrodevil profile image
Astrodevil

Thanks

Collapse
 
xtitan profile image
Abhinav

Great Share

Collapse
 
astrodevil profile image
Astrodevil

Thanks!

Collapse
 
devabhi404 profile image
DevAbhi

pretty detailed guide, helpful one

Collapse
 
astrodevil profile image
Astrodevil

Thankyou

Collapse
 
ddebajyati profile image
Debajyati Dey

Interesting tutorial.
Thanks for sharing.

Collapse
 
astrodevil profile image
Astrodevil

Glad you found interesting!