DEV Community

Cover image for Build a Collaborative Airtable-Style Data Grid in Next.js using AG Grid and Velt SDK
Astrodevil
Astrodevil

Posted on

Build a Collaborative Airtable-Style Data Grid in Next.js using AG Grid and Velt SDK

Have you ever attempted to build a collaborative app and struggled to implement real-time updates and collaboration? An app, similar to Google Docs, Figma, Frame.io, or Airtable. Having to deal with all of the collaboration features yourself, from WebSockets to concurrency to performance optimizations, can turn what sounds like a simple feature into weeks or even months of work.

Forget schema design for a moment. The real challenge is in managing:

  • Syncing every cell edit between concurrent users.
  • Handling live presence and cursors across the grid.
  • Ensuring instant synchronization and session resilience, so if a user goes offline or loses connection, you can automatically manage the state, reconnect, and sync all changes safely upon return.

Building this real-time infrastructure from scratch is a massive, daunting project.

In this tutorial, you'll learn how to build an Airtable-style collaborative app using Next.js and AG Grid to provide the powerful, editable data table component and Velt SDK for collaboration.

Airtable landing page

Airtable is a popular spreadsheet-database hybrid well known for its powerful data management and rich collaboration features. For example, in Airtable, team members can add comments directly to specific records or fields to suggest changes or discuss details.

Building this app will help you learn how to add live user presence, inline commenting, and real-time editing without worrying about any backend complexity. By the end, you will have a fully interactive table that multiple users can edit and comment on simultaneously.

Prerequisites

To follow along with the tutorial, you need the following:

Technical Challenges of Building Collaborative Tables

Building collaboration features from scratch in an application like Airtable is a lot more than it seems. Most teams underestimate the complexity, and what seems like a simple collaborative app becomes weeks, if not months, of infrastructure work and maintenance.

You have to manage:

  • Real-time collaborative editing across multiple users
  • Inline commenting with contextual references
  • User presence indicators and activity tracking
  • Simultaneous cell editing by multiple users, resolving edit conflicts.

Image to show the process of building a collaborative app

Each of these functionalities requires a dedicated backend infrastructure, including WebSocket servers, CRDT storage, and meticulous state management, as well as handling edge cases such as network disconnections and concurrent sessions. But with Velt, we can achieve this with just a couple of lines of SDK setup.

How Velt Solves These Challenges?

Velt Dashboard

Velt is a collaboration SDK that enables developers to integrate real-time features, including comments, cursors, presence indicators, and shared editing, into any web app.

It automatically handles synchronization, session management, and event broadcasting, allowing the developer to focus on the user interface.

The Velt React SDK enables us to:

  • Identify and track active users.
  • Sync edits across clients.
  • Show real-time presence and changes.
  • Manage document sessions for collaborative UIs.

With these features, we can easily build an Airtable-like collaborative grid application. This makes it easy for teams to collaborate on data without needing to leave the application. Without wasting any more time, let's get started and begin building.

Building With AG Grid

To build the table for our Airtable app, we will use AG Grid, a customizable React Data Grid library for building **React Tables.**

It comes with a ton of features and no third-party dependencies. Some of the features include rich grid functionalities, column resizing and reordering, cell editing, and row pinning.

Project Setup

In this section, we will integrate the Velt SDK to transform a simple table component built with AG grid library into a fully real-time, multi-user experience, complete with live collaborative features, editable rows and columns, and synchronized user interactions.

Before you start coding, you need to set up your Velt environment. Log in to Velt to retrieve your api key.

Velt Dashboard

The next step is to safelist your domains. For this demo, only add localhost. You can safelist domains in the Velt Console by entering your app’s deployment URL under the Allowed Domains section. This is a required action and ensures your application can communicate securely with Velt before going live with Velt features.

The live code is available on GitHub. You can fork, clone, and run locally. This article focuses on the key aspects of the demo application code and explains how the core functionalities of Velt are implemented. This will provide you with an in-depth understanding of how Velt features work, allowing you to start building other collaborative apps.

Here’s a link to the live demo to check it out: https://airtable-velt.vercel.app/

Follow these steps to run the application locally:

  1. Clone the repository:
git clone [https://github.com/Studio1HQ/airtable-velt.git](https://github.com/Studio1HQ/airtable-velt)
cd airtable-velt
Enter fullscreen mode Exit fullscreen mode
  1. Next install dependencies
npm install
Enter fullscreen mode Exit fullscreen mode
  1. Create a .env.local file in the root folder and add your Velt API key:
NEXT_PUBLIC_VELT_ID=your-api-key
Enter fullscreen mode Exit fullscreen mode

Here's how the code structure will look:

airtable-velt/
├── app/
   └── (app)/
       ├── layout.tsx`  // 1. Global VeltProvider Integration
│       └── page.tsx
│   ├── global.css
│   └── layout.tsx
├── components/
│   └── (general)/
│       ├── top-bar.tsx  // 2. User Identify, Presence, Notifications
│       ├── data-grid.tsx// 3. AG Grid Table, Velt Comments/Tools
│       └── ...          // Other UI components (sidebar, toolbar, etc.)
├── hooks/
│   └── use-theme.ts
├── lib/
│   ├── utils.ts
│   └── constant.ts      // Contains initial row data
└── .env.local           // Stores NEXT_PUBLIC_VELT_API_KEY
Enter fullscreen mode Exit fullscreen mode
  1. Start the development server:
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open your browser and navigate to http://localhost:3000 to see the app in action. Once the application is up and running, you’ll see a clean, Airtable-style interface that allows you to view and manipulate data in real-time.

Demo app view

When you open the app, you can try out connecting two or more users simultaneously, for example, in different browser windows. Thanks to Velt’s SDK, every action you take, like editing a cell or adding a row, appears instantly for all connected users.

Let’s get to the main code and discuss the few lines of code that make this seamless collaboration possible.

Wrap your App with VeltProvider

In the app/layout.tsx, we wrap the main component with the VeltProvider component like this:

"use client";

import { ThemeProvider } from "@/hooks/use-theme";

import { VeltProvider } from "@veltdev/react";

export default function RootLayout({

children,

}: {

children: React.ReactNode;

}) {

return (

    <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_ID || ""}>

    <ThemeProvider>{children}</ThemeProvider>

    </VeltProvider>

    );

}
Enter fullscreen mode Exit fullscreen mode

By passing your apiKey to the VeltProvider component, you initialize the Velt client and establish a persistent, secure connection to the Velt real-time service for the entire application session.

Identify Users

In a collaborative app, Velt needs to know who the user is and which document they are viewing. We handle this initialization in the main header component, components/(general)/top-bar.tsx. We first predefined two users in the helper/userdb. The predefinedUsers function returns the users details. The useEffect then stores them in the local storage so that they can be easily retrieved.

const { user, setUser } = useUserStore();

const predefinedUsers = useMemo(
    () =>
      userIds.map((uid, index) => {
        const avatarUrls = [
          "https://api.dicebear.com/7.x/pixel-art/svg?seed=Nany",
          "https://api.dicebear.com/7.x/pixel-art/svg?seed=Mary",
        ];
        return {
          uid: uid,
          displayName: names[index],
          email: `${names[index].toLowerCase()}@gmail.com`,
          photoUrl: avatarUrls[index],
        };
      }),
    []
  );

   // Initialize user from localStorage if none exists
  useEffect(() => {
    if (typeof window !== "undefined" && !user) {
      const storedUser = localStorage.getItem("user-storage");
      if (!storedUser) {
        setUser(predefinedUsers[0]);
      }
    }
  }, [user, setUser, predefinedUsers]);
Enter fullscreen mode Exit fullscreen mode

Initialize Velt

The next step is to identify the current logged in user and initialize the Velt client. In the tool-bar.tsx file, you will find the following code:

import {
  useVeltClient,
  // other imports
} from "@veltdev/react";

const { client } = useVeltClient();
  const prevUserRef = useRef(user);
  const isInitializingRef = useRef(false); // Prevent overlapping initialization calls

  // Handle Velt client initialization, user identification, and document setting
  useEffect(() => {
    if (!client || !user || isInitializingRef.current) {
      console.log("Velt init skipped:", {
        client: !!client,
        user: !!user,
        initializing: isInitializingRef.current,
      });
      return;
    }

    const initializeVelt = async () => {
      isInitializingRef.current = true;
      try {
        // Detect user switch
        const isUserSwitch = prevUserRef.current?.uid !== user.uid;
        prevUserRef.current = user;

        console.log("Starting Velt init for user:", user.uid, { isUserSwitch });

        // Re-identify the user (handles initial and switches)
        const veltUser = {
          userId: user.uid,
          organizationId: "organization_id",
          name: user.displayName,
          email: user.email,
          photoUrl: user.photoUrl,
        };
        await client.identify(veltUser);
        console.log("Velt user identified:", veltUser.userId);
        await client.setDocuments([
          {
            id: "airtable-inventory",
            metadata: { documentName: "airtable-inventory" },
          },
        ]);
        console.log("Velt documents set: airtable-inventory");
      } catch (error) {
        console.error("Error initializing Velt:", error);
      } finally {
        isInitializingRef.current = false;
      }
    };

    initializeVelt();
  }, [client, user]); 
Enter fullscreen mode Exit fullscreen mode

In the code snippet above:

  • The initializeVelt function initializes the Velt SDK.
  • The current logged in user is identified with client.identify() method.
  • The app sets the document "airtable-inventory" using client.setDocuments(). This defines the collaboration scope.
  • Velt manages real-time collaboration, inline comments, and presence tracking automatically.

At the last lines of the UI code, we have:

<VeltPresence />
<VeltSidebarButton darkMode={theme === "dark"} />
<VeltCommentsSidebar darkMode={theme === "dark"} />
Enter fullscreen mode Exit fullscreen mode

These three components are prebuilt Velt’s components. Here’s what they do:

  • VeltPresence - The VeltPresence component is a UI element that displays the avatars of users who are currently active in the same document.
  • VeltCommentsSidebar - The Sidebar that holds a list of all the existing comments.
  • VeltSidebarButton - A button that toggles the VeltCommentsSidebaron and off.

Create an Editable Data Grid

In the data-gri.tsx we use the AG Grid library for the table, and just like Airtable, each cell have inline comments, but this time enabled by the VeltCommentTool.

import { useSetDocument, VeltComments, VeltCommentTool } from "@veltdev/react";
import { AgGridReact } from "ag-grid-react";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
import { useMemo, useState, useRef, useCallback } from "react";
import { rowdata } from "@/lib/constant";

ModuleRegistry.registerModules([AllCommunityModule]);

const TASK_HEADERS = ["ID", "Name", "Notes", "Assignee", "Status", "Attachments", "Attachment Summary", "assigneeColor"];

export default function DataGrid() {
  const [rowData] = useState(() => rowdata);
  const gridRef = useRef<AgGridReact>(null);

  // Link the document for Velt
  useSetDocument("airtable-inventory", { documentName: "stock-management" });

  const columnDefs = useMemo(() =>
    TASK_HEADERS.map((header) => ({
      field: header,
      headerName: header,
      editable: true,
      cellRenderer: EditableCellRenderer
    })),
    []
  );

  return (
    <div className="ag-theme-alpine" style={{ height: 500 }}>
      <VeltComments popoverMode customAutocompleteSearch darkMode={false} />
      <AgGridReact
        ref={gridRef}
        rowData={rowData}
        columnDefs={columnDefs}
      />
    </div>
  );
}

// Inline editable cell with Velt comment tool
const EditableCellRenderer = ({ value, node, colDef, data }) => {
  const [val, setVal] = useState(value);
  const cellId = `cell-${node.rowIndex}-${colDef.field}`;

  const onBlur = useCallback(() => node.setDataValue(colDef.field, val), [val]);

  const assigneeColor = colDef.field === "Assignee" ? data.assigneeColor : undefined;

  return (
    <div id={cellId} data-velt-target-comment-element-id={cellId}>
      <input
        value={val}
        onChange={(e) => setVal(e.target.value)}
        onBlur={onBlur}
        style={{ backgroundColor: assigneeColor ?? "transparent" }}
      />
      <VeltCommentTool targetElementId={cellId} darkMode={false} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here’s what the code above does:

  • useSetDocument ensures comments are tied to the "airtable-inventory" document.
  • Each input has data-velt-target-comment-element-id, so Velt knows where to attach comments.
  • <VeltComments /> renders all comments for the document.
  • <VeltCommentTool /> allows inline commenting per cell.

That’s it, with just a few hooks and components imported from Velt, your application instantly gains powerful, enterprise-grade collaboration features. You now have real-time collaboration in editable grids, enabling multi-user sync across cells, inline comments on each cell for contextual feedback, presence indicators that clearly show online users, dedicated tools for notifications, and a comprehensive comment sidebar.

The rest of the code, the editable cell logic, theme switching, and structural sidebars, is standard React and Next.js code. This powerful division of labor means Velt handles the entire real-time communication infrastructure, allowing you to focus exclusively on building a polished, responsive front-end interface.

Running the Application

Run npm start to start the development server, access the app at:

http://localhost:3000

Want to see how it works? Here's a quick preview: https://airtable-velt.vercel.app

Explore the full source code on GitHub to dive deeper into the implementation.

Final Thoughts

In this tutorial, we built a lightweight Airtable-like collaborative application using the Velt SDK.

We achieved:

  • Real-time cell editing is shared among multiple users
  • Presence and awareness of the user
  • Dynamic table structure: add/remove rows and columns
  • Instant synchronization without a custom backend

What traditionally takes weeks of infrastructure and socket setup can now be done with a few lines of Velt integration. You can delve into advanced Velt features, such as reactions and presence cursors. You can also customize your table's UI with sorting, filters, and permissions, and deploy your own collaborative workspace using Velt as the backbone for real-time collaboration.

Resources

Top comments (0)