DEV Community

Cover image for Building a Typefully Clone with Real-Time Collaboration
Arindam Majumder
Arindam Majumder Subscriber

Posted on

Building a Typefully Clone with Real-Time Collaboration

Introduction

Typefully, a writing and scheduling platform, helps creators and marketing teams build their brands on X (formerly Twitter) and LinkedIn. It also features collaborative tools, such as adding comments or notes to post drafts, which could be very useful for social media teams.

Image

Managers need to know who’s working on what. Team members need notifications for when they’re mentioned or assigned new tasks. Without this, workflows break down, leading to miscommunications and delays.

Large marketing teams need a platform with more collaboration features.

This article guides you through building a Typefully clone supercharged with real-time collaboration features, such as notifications, mentions, and commenting.

Project Setup and Structure

In this section, you’re going to set up your project. For this tutorial, we’ll be using the following tech stack:

  • Next.js: full-stack framework for building the app.
  • Tiptap: for adding a rich text editor to our app.
  • Velt: for adding the collaboration features.
  • Shadcn: for UI components.
  • Zustand: for state management.

Use the following command to scaffold a Next.js:

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

The --yes flag skips the prompts and creates a new Next.js project with saved preferences and defaults. The default setup enables TypeScript, Tailwind, ESLint, App Router, and Turbopack, with import alias @/*. This setup is enough for this tutorial.

Next, install the necessary packages for your app.

Run the following command to install Tiptap, Velt, and Zustand:

npm install zustand @tiptap/react @tiptap/starter-kit @veltdev/react @veltdev/tiptap-velt-comments
Enter fullscreen mode Exit fullscreen mode

Here’s what these packages are for:

  • zustand: the Zustand library for state management.
  • @tiptap/react: Tiptap package for React-based frameworks.
  • @tiptap/starter-kit: a starter kit for the Tiptap editor.
  • @veltdev/react: Velt SDK for React-based frameworks.
  • @veltdev/tiptap-velt-comments: Velt package for adding comments in a Tiptap editor.

Now that you’re done with setting up your project, the next sections show you how to build the Typefully clone. Before that, let’s see why we’re using Velt for the collaboration features.

Why Velt for collaboration features?

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:

  • Design an intuitive, user-friendly UI for comments and user avatars.
  • Handle the dynamic and complex interactions, such as text selection, popovers, user tracking, real-time updates, and push notifications.
  • Manage comment and notification annotation functionalities and storage.

Handling these tasks yourself will slow down your development process.

Velt solves this problem for you. Velt offers ready-made full-stack components that enable you to add real-time collaboration features to your app without writing huge lines of code. Velt handles the syncing of the UI and backend, storage, and push notifications. This allows you to focus on the core features that make your app unique.

Implementing the core editor using Tiptap

In this section, you’ll add the Tiptap editor to your Next.js app.

Let me point out something before we proceed. The code snippets in this tutorial focus on the important parts of building the Typefully clone. The UI and styling parts will be skipped. For the full working code, check out the repo.

To add Tiptap to your app, you’ll create the following components:

  • PutThread: initializes and renders the editor UI.
  • PostCard: renders the posts and PutThread if a certain condition is met.
  • ThreadView: renders the PostCard component.

Also, you’ll configure Zustand to manage user state. This tutorial uses a mock user database. In your app, you’d integrate Zustand with your authentication system.

Configuring the Zustand store for user management

Create a new folder, helper, in the root directory of your app. Inside helper, create a userdb.ts file.

Inside the userdb.ts file, add the following code:

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

Let’s understand what’s happening in the code:

  • userIds and names are arrays of hardcoded users. In your app, you’d fetch users from your auth function or database.
  • create is a function from Zustand for creating a 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.

Next up is creating the PutThread component.

Creating the PutThread component

Like I mentioned earlier, PutThread initializes and renders the Tiptap editor.

Create a components folder. Inside the components folder, create a PutThread.tsx file and add the following code:

"use client";

import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import { StarterKit } from "@tiptap/starter-kit";
import { Button } from "../ui/button";
import { MessageCircle } from "lucide-react";

const PutThread = () => {
  // Initialize Tiptap editor
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: `
    <article class="post">
  <header>
    <h2>Building an Audience: A Compounding Investment</h2>
    <p>Building an audience is a great investment that compounds over the years, maximizing the impact of everything else you do online.</p>
  </header>

  <section aria-labelledby="tips-title">
    <h3 id="tips-title">Tips to get started</h3>
    <ul>
      <li>🚀 <strong>Start small</strong></li>
      <li>📋 <strong>Consistently deliver value</strong></li>
      <li>✨ <strong>Watch your influence multiply</strong></li>
    </ul>
  </section>

  <footer>
    <p>
      This is why
      <a href="https://typefully.com" target="_blank" rel="noopener">@typefully</a>
      is an investment in your future reach and impact.
    </p>
  </footer>
</article>
    `,
    autofocus: true,
  });

  // Comment handler (will be enhanced later with Velt)
  const onClickComments = () => {
    // Basic comment functionality - to be enhanced in next section
    console.log("Comment button clicked");
  };

  return (
    <div className="border-2 p-4 my-3 border-dashed rounded">
      {/* Bubble Menu with comment button */}
      {editor && (
        <BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
          <div className="bubble-menu">
            <Button
              variant="outline"
              onClick={onClickComments}
              className="bg-[#b056ef] hover:bg-[#a22ff5] p-2 flex items-center justify-center rounded-full focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-300"
            >
              <MessageCircle color="white" />
            </Button>
          </div>
        </BubbleMenu>
      )}

      {/* Editor Content */}
      <EditorContent editor={editor} />
    </div>
  );
};

export default PutThread;
Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening in this code:

  1. You defined a PutThread component that renders a rich text editor. The component uses the useEditor hook from Tiptap to initialize and manage the editor's state. useEditor accepts a configuration object that defines the editor's behavior, available features, and initial content.
  2. You configured the editor instance by passing an object to useEditor. This object contains:
    • extensions: an array that enables specific editor capabilities. Here, you include StarterKit, which provides common formatting options like bold, italics, lists, and headings.
    • content: the initial HTML content that populates the editor when it first loads.
    • autofocus: a boolean that determines if the editor should automatically receive focus when mounted. It’s set to true, which makes the editor immediately ready for input.
  3. You set up a bubble menu that appears when text is selected. The BubbleMenu component from Tiptap wraps the custom comment button, which will be used to add comments directly in the editor. The tippyOptions prop controls the animation behavior of the menu.
  4. You created an onClickComments handler function that currently logs a message to the console. This function provides the foundation for adding the comment functionality to the editor.
  5. You returned the JSX structure that renders the editor interface. The component includes:
    • The conditional BubbleMenu containing a styled button with a message icon.
    • The EditorContent component that renders the actual editable area where users can interact with the text.

This component creates a fully functional rich text editor with a floating toolbar.

Creating the PostCard component

Now that you’ve created the PutThread component, you’ll create the PostCard component. This component will conditionally render the PutThread component.

Inside your components folder, create a PostCard.tsx file and add the following code:

"use client";

import PutThread from "./PutThread";
import { useUserStore } from "@/helper/userdb";

interface PostCardProps {
  author: {
    name: string;
    username: string;
    avatar: string;
    verified?: boolean;
  };
  timestamp?: string;
  content?: string;
  isThread?: boolean;
  showConnector?: boolean;
}

export function PostCard({
  author,
  timestamp,
  isThread,
  content,
  showConnector = false,
}: PostCardProps) {
  const { user } = useUserStore();

  return (
    <div className="p-3 sm:p-4 transition-shadow">
      <div className="flex items-start gap-3 relative">
        {/* Avatar and user info */}
        <div className="">
          {/* ... avatar component */}
        </div>

        {/* Content column */}
        <div className="flex-1 min-w-0">
          {/* ... user header info */}

          {/* Post content */}
          <div className="prose prose-sm dark:prose-invert max-w-none">
            <p className="whitespace-pre-line text-sm leading-relaxed mb-3">
              {content}
            </p>
          </div>

          {/* Conditionally render Tiptap editor */}
          {isThread && <PutThread />}
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's understand what's happening in the code:

  1. You defined a PostCard component that renders a social media-style post interface. The component accepts props, including author information, timestampcontent, and flags to control the display of threaded content. The isThread prop determines whether to show the Tiptap editor for content creation.
  2. You initialized user data using the useUserStore hook, which provides access to the current user's information, including display name and avatar.
  3. You conditionally render the PutThread component when isThread is true. This integrates the Tiptap rich text editor we configured earlier, allowing users to create and edit threaded content.

This component creates a reusable post interface that can display both simple content and rich text editor threads.

Creating the ThreadView component

Now that you’ve created the PostCard and PutThread components, you’ll create the ThreadView component. The ThreadView component renders the PostCard component.

Inside your components folder, create a thread-view.tsx file and add the following code:

"use client";

import { PostCard } from "./post-card";

const threadPosts = [
  {
    author: {
      name: "Fabrizio Rinaldi",
      username: "linuz90",
      avatar:
        "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=400",
      verified: true,
    },
  },
  {
    author: {
      name: "Fabrizio Rinaldi",
      username: "linuz90",
      avatar:
        "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=400",
      verified: true,
    },
    content: `By the way, this is a sample draft!`,
  },
  {
    author: {
      name: "Fabrizio Rinaldi",
      username: "linuz90",
      avatar:
        "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=400",
      verified: true,
    },
    content: `Some more tips for you:

  • Use the buttons in the top right to share & organize drafts
  • Drag & drop pictures and videos in the editor
  • Type : followed by an email alias
  • Paste a tweet link to quote it`,
  },
];

export function ThreadView() {
  return (
    <div className="max-w-2xl mx-auto p-4 sm:p-6">
      <div className="space-y-0">
        {threadPosts.map((post, index) => (
          <div key={index} className="relative">
            <PostCard
              {...post}
              isThread={index === 0}
              showConnector={index < threadPosts.length - 1}
            />
          </div>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening in this code:

  1. You defined a ThreadView component that renders a threaded conversation interface similar to X (formerly Twitter). The component uses a threadPosts array containing mock data. In your app, you’d fetch these posts from your database.
  2. Mapped through the threadPosts array to render each post using the PostCard component you created earlier. For each post, we pass the author data and content while dynamically setting two key properties:
    • isThread: set to true only for the first post, enabling the Tiptap rich text editor.
    • showConnector: set to true for all posts except the last one, creating visual connections between consecutive posts.
  3. You rendered the complete thread interface, where the first post contains your Tiptap editor (via isThread={true}) and subsequent posts display static content. This creates a realistic social media thread experience with the foundational editor integrated.

This component creates a complete threaded conversation view similar to X.

Displaying the threaded conversation in the UI

With the components created, you need to display the ThreadView in the UI. ThreadView is a wrapper of the other two components.

In your app’s home page, page.tsx, add the following code:

"use client";

import { useState } from "react";
import { Header } from "@/components/layout/header";
import { Sidebar } from "@/components/layout/sidebar";
import { ThreadView } from "@/components/post/thread-view";

export default function Home() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const toggleSidebar = () => {
    setSidebarOpen(!sidebarOpen);
  };

  const closeSidebar = () => {
    setSidebarOpen(false);
  };

  return (
    <div className="min-h-screen bg-background">
      <Header onToggleSidebar={toggleSidebar} />
      <div className="flex">
        <Sidebar isOpen={sidebarOpen} onClose={closeSidebar} />
        <main className="flex-1 overflow-auto min-w-0">
          <ThreadView />
        </main>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is the entry point of our application. It contains the threaded post interface.

With that, your UI should look like this:

Image

Remember, the full code is in the GitHub repo.

Now that you’ve created the UI and added the Tiptap editor to your app, you’re going to add real-time collaboration features in the next section.

Adding Real-Time Collaboration with Velt

Now that you’ve built the UI of the clone, you’ll add collaboration features using Velt next.

Before you add the collaboration features, let me explain a few key concepts to consider when working with Velt.

First, you need an API key to work with Velt. Go to Velt’s console and get your free API key.

Image

Note: you need to upgrade to the paid version before using the Velt API key in production.

Add the API key to your .env file:

NEXT_PUBLIC_VELT_ID="your-api-key"  
Enter fullscreen mode Exit fullscreen mode

With that out of the way, let’s get into adding the collaboration features.

Configuring Velt

Velt provides a Provider component to enable Velt in your app.

Update your page.tsx file to wrap your app inside VeltProvider and pass in your API key:

"use client";

import { useState } from "react";
import { Header } from "@/components/layout/header";
import { Sidebar } from "@/components/layout/sidebar";
import { ThreadView } from "@/components/post/thread-view";
import { VeltComments, VeltProvider } from "@veltdev/react";
import useTheme from "@/hooks/use-theme";

export default function Home() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const toggleSidebar = () => {
    setSidebarOpen(!sidebarOpen);
  };

  const closeSidebar = () => {
    setSidebarOpen(false);
  };
  const { theme } = useTheme();
  return (
    <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_ID!}>
      <div className="min-h-screen bg-background">
        <Header onToggleSidebar={toggleSidebar} />
        <div className="flex">
          <Sidebar isOpen={sidebarOpen} onClose={closeSidebar} />
          <main className="flex-1 overflow-auto min-w-0">
            <ThreadView />
          </main>
        </div>
      </div>
    </VeltProvider>
  );
}

Enter fullscreen mode Exit fullscreen mode

These are the collaboration features that you’ll be adding to your app:

  • Comments
  • Presence
  • Notification

Let’s tackle them one after the other.

Adding comments

Velt requires authentication to identify your users. Without proper authentication and document initialization (we’ll do that in a bit), Velt won’t work.

For this tutorial, we’ll simulate fetching a list of users and then use the client.identify() method to identify the user with Velt.

The logic for authentication will be in the Header component. In your app, you can separate the logic into its own component.

Create a header.tsx file inside the components folder and add the following code:

export function Header({ onToggleSidebar }: HeaderProps) {
  const { user, setUser } = useUserStore();
  const { client } = useVeltClient();

  // Velt user identification
  useEffect(() => {
    if (!client || !user) return;

    const initializeVelt = async () => {
      // Identify user with Velt
      const veltUser = {
        userId: user.uid,
        name: user.displayName,
        email: user.email,
        photoUrl: user.photoUrl,
      };
      await client.identify(veltUser);

      // Set document for comment scope
      await client.setDocuments([
        {
          id: "typefully-comments",
          metadata: { documentName: "typefully-comments" },
        },
      ]);
    };

    initializeVelt();
  }, [client, user]);

  // ... rest of component
}
Enter fullscreen mode Exit fullscreen mode

Let’s understand what’s happening in the code:

  1. You created a user dropdown component that allows switching between predefined users. The component uses the useVeltClient hook to access the Velt client instance, which handles the real-time connection and user management.
  2. You defined mock user data with unique user IDs, display names, and avatar URLs. The mock user database provides the necessary user information that Velt requires to identify users and display their avatars when they’re online.
  3. The useEffect hook initializes Velt when both the client and user data are available. The hook detects user switches and re-identifies the user with Velt using the client.identify() method. Velt requires these fields: userId, name, email, organization, and photoUrl to identify users.
  4. You configured the document context using client.setDocuments() with a unique document ID (”typefully-comments”). This scopes the collaboration features to a specific document or thread, which means comments and presence are isolated between different content areas.

This authentication setup allows Velt to track your users across all collaboration features, which allows for personalized comments, presence indicators, and real-time updates throughout your application.

Adding the Comments Sidebar

The Comments Sidebar provides a toggleable sidebar to view and filter comments. You need two components to add a comments sidebar to your app:

  • VeltCommentsSidebar: the sidebar component that contains all comments in a document (in this case, the content area).
  • VeltSidebarButton: a button to toggle the VeltCommentsSidebar on and off.

Update the Header component by adding the VeltCommentsSidebar and VeltSidebarButton components.

// header.tsx

"use client";

// ... other imports
import {
  VeltCommentsSidebar,
  VeltSidebarButton,
} from "@veltdev/react";
// ... other imports
import useTheme from "@/hooks/use-theme";

export function Header({ onToggleSidebar }: HeaderProps) {
  // ... user authentication and Velt initialization logic

  const { theme } = useTheme();

  return (
    <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="flex h-14 items-center justify-between px-4">
        {/* ... header left section (logo, menu button) */}

        <div className="flex items-center justify-end gap-3 lg:gap-4">
          {/* Add Comments Sidebar and Toggle Button */}
          <VeltCommentsSidebar darkMode={theme === "dark"} />

          {/* ... user dropdown and other header controls */}

          <VeltSidebarButton darkMode={theme === "dark"} />
        </div>
      </div>
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adding comments to the Tiptap Editor

In this part of adding comments to your app, you’ll add comments to the Tiptap editor.

Velt provides an extension and hooks for adding comments in a Tiptap editor.

Go to the PutThread component, where you initialized the Tiptap editor, and update the code:

"use client";

// ... existing Tiptap imports
import {
  TiptapVeltComments,
  renderComments,
  addComment,
} from "@veltdev/tiptap-velt-comments";
import { useCommentAnnotations } from "@veltdev/react";
import { useEffect } from "react";

const EDITOR_ID = "typefully-comments";

const PutThread = () => {
  const editor = useEditor({
    extensions: [
      TiptapVeltComments.configure({
        persistVeltMarks: false,
      }),
      // ... existing extensions
    ],
    // ... existing configuration
  });

  // Velt comment integration
  const annotations = useCommentAnnotations();

  useEffect(() => {
    if (editor && annotations?.length) {
      renderComments({
        editor,
        editorId: EDITOR_ID,
        commentAnnotations: annotations,
      });
    }
  }, [editor, annotations]);

  const onClickComments = () => {
    if (editor) {
      addComment({
        editor,
        editorId: EDITOR_ID,
      });
    }
  };

  return (
    // ... existing UI structure
    // Bubble menu now functional with Velt comments
  );
};

export default PutThread;
Enter fullscreen mode Exit fullscreen mode

Let’s understand the updated part of the code:

  • EDITOR_ID represents the unique ID of the Tiptap editor. EDITOR_ID should match the value organizationId field of the veltUser object.
  • TiptapVeltComments extension adds commenting functionality within the editor. It is configured so that Velt marks are not persisted in the editor.
  • The useCommentAnnotations hook fetches comment data from Velt.
  • renderComments inside the useEffect hook renders the comments in the Tiptap editor. renderComments accepts an object as its parameter with the following properties:
    • editor: instance of the Tiptap editor.
    • editorId: the Tiptap editor ID (EDITOR_ID).
    • commentAnnotations: array of Comment Annotation objects.
  • The addComment method inside the onClickComments function allows users to add comments to selected text in the Tiptap editor. addComment takes in two required parameters:
    • editor: instance of the Tiptap editor.
    • editorId: ID of the Tiptap editor.

Update the page.tsx file with the following code to enable comments in your app:

"use client";

import { useState } from "react";
import { Header } from "@/components/layout/header";
import { Sidebar } from "@/components/layout/sidebar";
import { ThreadView } from "@/components/post/thread-view";
import { VeltComments, VeltProvider } from "@veltdev/react";
import useTheme from "@/hooks/use-theme";

export default function Home() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  const toggleSidebar = () => {
    setSidebarOpen(!sidebarOpen);
  };

  const closeSidebar = () => {
    setSidebarOpen(false);
  };
  const { theme } = useTheme();
  return (
    <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_ID!}>
      <div className="min-h-screen bg-background">
        <Header onToggleSidebar={toggleSidebar} />
        <div className="flex">
          <Sidebar isOpen={sidebarOpen} onClose={closeSidebar} />
          <main className="flex-1 overflow-auto min-w-0">
            <ThreadView />
          </main>
        </div>
      </div>
      <VeltComments
        textMode={false}
        shadowDom={false}
        textCommentToolShadowDom={false}
        darkMode={theme === "dark"}
      />
    </VeltProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now your users can add comments directly inside the Tiptap editor.

It's important to know that by adding the VeltComments component, 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 Presence to your app.

Adding Presence

Presence allows users to see other users who are online. This feature makes your app feel like a chat application.

To add Presence to your app, you only need to do two things:

  • Import the VeltPresence from the @veltdev/react package.
  • Render the component where you’d like to see user avatars.

No extra configuration or setup.

Easy, right? I know.

Update your header.tsx file to include Presence:

"use client";

// ... other imports
import { VeltPresence } from "@veltdev/react";
// ... other imports

export function Header({ onToggleSidebar }: HeaderProps) {
  // ... existing user and theme logic

  return (
    <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="flex h-14 items-center justify-between px-4">
        {/* ... header left section */}

        <div className="flex items-center justify-end gap-3 lg:gap-4">
          {/* ... other components */}

          {/* Add Velt Presence to show live user avatars */}
          <VeltPresence />

          {/* ... other header buttons */}
        </div>
      </div>
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now users can see other users’ avatars who are online.

In the next subsection, you’ll add In-app Notifications.

Adding In-App Notifications

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

To add notifications to your app, you’ll have to enable notifications in your Velt console.

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

Image

Update your header.tsx file to add the VeltNotificationTool, which is the component for toggling the notification panel.

"use client";

// ... other imports
import { VeltNotificationsTool } from "@veltdev/react";
// ... other imports
import useTheme from "@/hooks/use-theme";

export function Header({ onToggleSidebar }: HeaderProps) {
  // ... existing user and theme logic
  const { theme } = useTheme();

  return (
    <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="flex h-14 items-center justify-between px-4">
        {/* ... header left section */}

        <div className="flex items-center justify-end gap-3 lg:gap-4">
          {/* ... other components */}

          {/* Add Notifications Tool for in-app alerts */}
          <VeltNotificationsTool darkMode={theme === "dark"} />

          {/* ... other header buttons */}
        </div>
      </div>
    </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:

Conclusion

You supercharged your Typefully clone with real-time commenting, presence, and in-app notification features.

The best part? You didn’t handle the backend yourself or build it from scratch. Velt took care of syncing, storage, and collaboration, letting you focus on the user experience.

If you’re working on your own SaaS, you can extend your app further by adding other powerful Velt features like screen recording, cursor tracking, reactions, and real-time hurdles. Adding these features can make your app more robust and give your users an even better experience.

Resources

Top comments (1)

Collapse
 
eleftheriabatsou profile image
Eleftheria Batsou

That's a great idea!