DEV Community

Cover image for How to Build Coda-Style Collaborative Features with Velt SDK and Shadcn
Astrodevil
Astrodevil

Posted on • Originally published at Medium

How to Build Coda-Style Collaborative Features with Velt SDK and Shadcn

Introduction

Coda is an all-in-one document platform that combines documents, spreadsheets, and apps into a single collaborative workspace. You can build databases, workflows, and interactive tools inside Coda. The cool thing is that you don’t need to build in isolation; you can collaborate, and this makes Coda even more powerful.

This tutorial guides you on how to build a collaborative Coda-like app using Next.js and Velt, a collaboration SDK.

Project Setup and Structure

In this section, you’ll set up your project. This tutorial uses the following tech stack:

  • Next.js: a full-stack framework for building apps.
  • Velt: for adding real-time collaboration features.
  • Zustand: for user state management.
  • Shadcn: for UI components.

Our final app will look like this:

Coda demo

Use the following command to create a new Next.js project:

npx create-next-app@latest my-app --yes
Enter fullscreen mode Exit fullscreen mode

--yes skips prompts using saved preferences or defaults. The default setup enables TypeScript, Tailwind, ESLint, App Router, and Turbopack, with import alias @/*.

Install the necessary packages:

npm install zustand @veltdev/react
Enter fullscreen mode Exit fullscreen mode

@veltdev/react is the Velt SDK for React-based frameworks.

Initialize Shadcn:

npx init shadcn
Enter fullscreen mode Exit fullscreen mode

In the next section, you’ll learn what Velt is and why you should use it for adding collaboration features to your apps.

Why choose Velt for real-time collaboration?

Velt

If you’ve built a collaborative web app from the ground up, you know how much complexity goes into building the collaboration features.

You have to set up servers, manage WebSocket connections and synchronization, and handle the UI state changes.

This can add unnecessary complexity and time delay to your development process.

Velt solves this problem for you. Velt provides ready-made full-stack components that allow you to add real-time collaboration features to your app without writing any backend code. Velt handles the UI and backend synchronization, storage, and push notifications. You get to focus on the core features that make your app unique.

Part 1: Building the authentication system

This is the first part of building the Coda-like app. But before you start building, let me point out a few things:

First, you need an API key from Velt. Get your API key from Velt’s console.

Velt Dashboard

You need to upgrade to the paid version to use Velt in production.

Second, Velt needs an authentication system to identify your app users. This tutorial uses a mock users array and Zustand to simulate a real authentication system.

Lastly, this tutorial focuses on the relevant code snippets and components for adding collaboration features. The UI and styling parts will be omitted for the most part. Check the Resources section for the full, working code.

Creating the User Store with Zustand

Create a userdb.ts file and set up a Zustand store with two mock users:

import { create } from "zustand";
import { persist } from "zustand/middleware";

export type User = {
  uid: string;
  displayName: string;
  email: string;
  photoUrl?: string;
};

export interface UserStore {
  user: User | null;
  setUser: (user: User) => void;
}

export const userIds = ["user001", "user002"];
export const names = ["Nany", "Mary"];

export const useUserStore = create<UserStore>()(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
    }),
    {
      name: "user-storage",
    }
  )
);
Enter fullscreen mode Exit fullscreen mode
  • userIds and names are arrays of hardcoded users. In your app, this will be part of your auth system.
  • create is a function from Zustand for creating the Zustand store. A Zustand store is a centralized location for managing your application’s state. The Zustand store has the shape of the UserStore interface.
  • persist, from Zustand, is used to “persist” users in the browser’s localStorage.
  • useUserStore is the hook you’ll use to interact with your Zustand store.

Initializing Velt with useEffect

Next, you initialize Velt and a user. You’ll handle the initialization inside useEffect hooks.

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

This hook:

  • Checks if you're in a browser environment (important for Next.js SSR).
  • Prevents redundant loads by checking if a user already exists.
  • Loads persisted user data from localStorage.
  • Sets a default user if none exists (for demo purposes).
// Handle Velt client initialization, user identification, and document setting
useEffect(() => {
  // Guard: only run when all prerequisites are met
  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 if the user has switched accounts
      const isUserSwitch = prevUserRef.current?.uid !== user.uid;
      prevUserRef.current = user;

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

      // Map our app user to Velt's user format
      const veltUser = {
        userId: user.uid,
        organizationId: "organization_id", // For multi-tenant apps
        name: user.displayName,
        email: user.email,
        photoUrl: user.photoUrl,
      };

      // Critical: Tell Velt who this user is
      await client.identify(veltUser);
      console.log("Velt user identified:", veltUser.userId);

      // Set the document context for collaboration
      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]); // Re-run only when client or user changes
Enter fullscreen mode Exit fullscreen mode

This second hook*:*

  1. Prevents race conditions and duplicate initializations.
  2. Detects when users change accounts (for demo purposes).
  3. Maps your user object to Velt's format. Velt expects the user object to have the following fields:
    • userId: Velt uses this to track user activity across sessions.
    • organizationId: enables multi-tenant features (teams, workspaces).
    • name & photoUrl: for personalization.
    • email: for notification delivery and @mention functionality.
  4. Tells Velt which "document" users are collaborating on.
  5. Handles network or API errors.

In the next section, you’ll add the collaboration features.

Part 2: Adding real-time collaboration features

This section is the fun part. You’ll configure Velt for your app and add the collaboration features.

Configure Velt in your app

Wrap your app inside the VeltProvider component to enable Velt in your app:

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

export default function WorkspacePage() {
  return (
    <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_ID || ""}>
      {/* Your app content */}
    </VeltProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adding real-time Comments

You’ll add the Comments feature in two components: Topbar and CodaTable.

The Topbar component is the topmost header of the app.

CodaTable is the table on the home page.

You can check the repo for the full code.

Add the Velt components to your Topbar component:

import { VeltCommentsSidebar, VeltSidebarButton } from "@veltdev/react";

function Topbar() {
  return (
    <header>
      {/* Other header content */}
      <VeltCommentsSidebar darkMode={theme === "dark"} />
      <VeltSidebarButton darkMode={theme === "dark"} />
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

You added the two Velt components:

  • VeltCommentsSidebar: holds all existing comments.
  • VeltSidebarButton: toggle button to show/hide the sidebar.

This creates a unified interface for discussion with minimal code.

Add the Velt Comments components to the CodaTable component:

import { VeltComments, VeltCommentTool } from "@veltdev/react";

function CodoTable() {
  return (
    <>
        {/* Other content of the table */}
      <VeltComments
        popoverMode
        customAutocompleteSearch
        darkMode={theme === "dark"}
      />

      <div>
        <VeltCommentTool
          targetElementId={`velt-comments-${member.name}+${member.email}+${member.id}+${index}`}
          style={{ width: 18, height: 18 }}
          darkMode={theme === "dark"}
        />
      </div>
      {/* Other content of the table */}
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

You added two key Velt commenting components to the workspace table:

  • VeltComments: Provides the global comment system with popover mode and custom autocomplete for team collaboration.
  • VeltCommentTool: Creates comment triggers on individual table cells with unique targetElementId identifiers for precise discussions.

This enables team members to discuss workspace member details directly on the relevant table cells.

Note: by adding VeltComments, you enable a range of collaboration features by default. These features include threaded replies, @mentions, read receipts, and being able to resolve comment threads.

In the next subsection, you’ll add the Live Presence feature.

Adding Live Presence

In this subsection, you’ll add live presence to your app. Presence allows users to see other users who are online in a particular document.

You’ll add the Velt component for showing users who are currently online in the Topbar component.

Update your Topbar component:

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

function Topbar() {
  return (
    <header>
      {/* Other header content */}
      <VeltPresence />
    </header>
  );
}

Enter fullscreen mode Exit fullscreen mode

You added the VeltPresence component to display active team members in real time. This component shows avatars of all currently active users of a particular document.

Adding In-App Notifications

In-app notifications allow users to receive email notifications when they’re mentioned in a document.

Enable notifications in your Velt console so that you can add notifications to your app.

Go to the Notifications section in the “Configurations” section of the Velt Console and enable Notifications.

image.png

Update your Topbar component to add the VeltNotificationTool, which is the component for toggling the notification panel.

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

function Topbar() {
  return (
    <header>
      {/* Other header content */}
      <VeltNotificationsTool darkMode={theme === "dark"} />
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Demo

Start your development server using the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open your browser, and you should see something like this:

Extending the app

The Coda-like app in this tutorial only implements core collaboration features, but you can do more with Velt’s SDK.

Below are some ways you use Velt’s collaboration features to create an even better app experience for your users:

Turn static data into a place of discussion

Your tables, charts, and reports are where decisions happen. Velt’s contextual commenting allows you to anchor discussions directly to the data.

For your SaaS: Add VeltCommentTool to database rows, chart data points, or Kanban cards. This turns your static dashboard into a live workspace where teams can discuss metrics, annotate outliers, and resolve issues with zero context switching. It replaces endless email sequences, Slack threads, and screenshots.

Guide and support with visual communication

Complex feedback often requires more than text.

For your SaaS: Add screen recording and canvas annotations. Imagine a user highlighting an area of a design mockup, recording a quick voice-over explaining a change, and pinning it directly to that element. This is invaluable for gathering product feedback, conducting design reviews, or providing customer support within your app, thereby reducing misinterpretation and cycle time.

Host live, contextual working sessions

Move beyond async comments to synchronous, focused collaboration.

For your SaaS: Enable real-time huddles (audio/video) and "single editor*"* mode. A team can jump on a quick call within the specific document or dashboard they're discussing, with one person driving the shared view. This mimics an "in-person whiteboard" experience for remote teams planning a roadmap or analyzing a dataset together.

Add a layer of ambient awareness

Presence is more than avatars; it’s about understanding team activity at a glance.

For your SaaS: Utilize live cursor tracking and presence indicators not only in documents, but also in admin panels, configuration settings, and project timelines. Seeing that a colleague is currently editing the campaign budget prevents conflicts and silently encourages parallel work on other sections.

Implementation mindset: Start with a workflow, not a Feature

  1. Identify a friction point: Where do your users currently copy-paste data into Slack or schedule a meeting to discuss something in your app?
  2. Map the Velt Component: Could a pinned comment (VeltComments), a live huddle (VeltHuddle), or a screen recording (VeltRecorder) resolve that friction in-place?
  3. Embed and iterate: Follow the same pattern used in the demo: identify users, set document context, and drop in the collaborative component. The complex real-time infrastructure is handled for you.

This will make your app feel more alive, and users will love it.

Conclusion

Building a good SaaS product today requires more than a sleek single-player experience. Users expect seamless, real-time collaboration they enjoy in tools like Coda, Figma, and Google Docs. To add collaboration features to your app, you’ll handle the backend infrastructure, WebSocket connections, and UI synchronization.

This tutorial showed that that’s not the case. By using the Velt SDK, you integrated user presence, real-time comments, and notifications without writing any backend code.

So, whether you're building the next great project management tool, a marketing platform, or an internal dashboard, you can now add collaboration features that feel native. This keeps teams engaged and productive inside your product.

Resources

Top comments (1)

Collapse
 
member_fc281ffe profile image
member_fc281ffe

The async/await mental model shift is real. What trips people up most is still error handling — unhandled promise rejections in event-driven code can be silent until they aren't. Centralizing rejection handling at the top level rather than .catch() on every single chain makes a huge difference in practice.