Build a Real-Time Miro Clone With Nextjs, React, Tailwind (2024)

Code With AntonioAbout 15 min readMay 27, 2025Watch original
THE SUMMARYAI-generated

Realtime Collaborative Whiteboard with Next.js 14: Detailed Summary

Key Concepts:

  • Realtime collaboration
  • Next.js 14
  • Convex (backend and database)
  • Clerk (authentication)
  • Liveblocks (realtime presence and collaboration)
  • Shadcn UI (component library)
  • Tailwind CSS (styling)
  • Server Components
  • File and Folder Based Routing
  • JWT Templates
  • Stripe (for SaaS implementation - mentioned but not covered in detail)

1. Introduction and Project Overview:

  • The project is a realtime collaborative whiteboard application, a "Miro clone," built with Next.js 14.
  • It allows multiple users to interact with the same content simultaneously, including sticky notes, wireframing, and drawing.
  • The goal is to create a digital whiteboard for brainstorming, planning, and team collaboration, regardless of location.
  • The tutorial covers the entire development process, from setting up the project to implementing realtime features.
  • A SaaS extension with Stripe integration is mentioned as additional content.

2. Features Demonstrated:

  • Realtime Sticky Notes: Simulates a sticky note meeting where users can add, move, and update notes in realtime.
  • App Wireframing: Enables visual representation of app ideas with drawing, layering, and repositioning of elements.
  • Drawing: Allows freeform drawing with various colors and tools for creative brainstorming.
  • Organization: Includes "move to back" and "bring to front" features for managing element order.
  • Collaboration: Supports favoriting, organization creation, and team member invitations.

3. Technology Stack:

  • Next.js 14: React framework for building the application.
  • React: JavaScript library for building user interfaces.
  • Tailwind CSS: Utility-first CSS framework for styling.
  • Shadcn UI: Collection of reusable components for UI elements.
  • Convex: Backend and database for realtime data synchronization.
  • Liveblocks: Infrastructure for realtime presence and collaboration.
  • Clerk: Authentication and user management.

4. Setting Up Next.js 14:

  • System Requirements: Verify Node.js version using node -v.
  • Automatic Installation: Use npx create-next-app@latest to create a new Next.js project.
    • Project name: User-defined (e.g., "board-video-tutorial").
    • TypeScript: Yes.
    • ESLint: Yes.
    • Tailwind CSS: Yes.
    • Source directory: No.
    • App Router: Yes (crucial for React Server Components).
    • Import alias: No (or customize if needed).
  • Open Repository: Open the created folder in a code editor (e.g., VS Code).
  • Install Shadcn UI: Use npx shadcn-ui@latest init.
    • Style: Default (recommended for Lucid icons).
    • Color: Slate (or any preferred color).
    • CSS variables: Yes.
  • Run Development Server: Use npm run dev to start the application on localhost:3000.

5. Working with Shadcn UI Components:

  • Shadcn UI is not a traditional component library; it's a collection of reusable components that are copied into the project.
  • Install a Component: Use npx shadcn-ui@latest add <component-name> (e.g., npx shadcn-ui@latest add button).
  • Component Location: Components are added to the components/ui folder (e.g., components/ui/button.tsx).
  • Import and Use: Import components in pages or other components using @/components/ui/<component-name>.
  • Customization: Modify the source code of Shadcn UI components directly to fit the design.
    • Variants, sizes, and class names can be customized.

6. Next.js Routing:

  • Top-Level Folders:
    • app: Contains the routing system, React Server Components, and interactive components.
    • public: Stores static assets (images, fonts, etc.).
    • pages (optional): Legacy routing system (not used in this tutorial).
    • src (optional): Source folder (not used in this tutorial).
  • Top-Level Files:
    • next.config.js: Configuration file for Next.js (e.g., extending webpack).
    • package.json: Lists project dependencies and scripts.
    • middleware.ts (optional): Intercepts routes for authentication or bot detection.
    • .env.local: Stores environment variables.
    • tailwind.config.js, tsconfig.json, .gitignore: Configuration files.
  • Routing Conventions:
    • page.tsx: Represents a page (route).
    • layout.tsx: Represents a layout that wraps a page or route segment.
    • loading.tsx: Represents a loading state.
    • error.tsx: Represents an error state.
    • route.ts: Used to create API endpoints.
  • Creating New Routes:
    • Create a new folder inside the app folder.
    • Add a page.tsx file inside the new folder.
    • Export a default React component from page.tsx.
  • Nested Routes: Create nested folders to create nested routes (e.g., app/test/subroute/page.tsx creates the route /test/subroute).
  • Dynamic Route Segments: Use square brackets to create dynamic route segments (e.g., app/users/[userId]/page.tsx creates the route /users/123).
    • Access dynamic parameters using the params prop in the page component.
  • Route Groups: Use parentheses to group routes without affecting the URL structure (e.g., app/(test)/subroute/page.tsx creates the route /subroute).
  • Omitting Folders from Routing: Use underscores to completely exclude a folder and its children from routing (e.g., app/_components/ui/button.tsx will not create a route).
  • Layouts:
    • Create a layout.tsx file inside a folder.
    • Export a default React component that accepts a children prop.
    • Render the children prop within the layout component.
    • Layouts are reusable and persist across route changes within their scope.

7. Setting Up Convex:

  • Create a Convex Account: Sign up at convex.dev.
  • Install Convex Package: Use npm install convex.
  • Initialize Convex: Use npx convex dev.
    • This command prompts for GitHub login, project creation, and saves production and deployment URLs.
    • It also creates a convex folder for backend API functions.
  • Run Convex Backend: Use npx convex dev in a separate terminal.
  • Run Frontend: Use npm run dev in another terminal.
  • Convex Dashboard: Access the Convex dashboard to view data, tables, and logs.

8. Setting Up Clerk:

  • Create a Clerk Account: Sign up at clerk.com.
  • Create a New Application:
    • Application name: User-defined (e.g., "board").
    • Select "Email address" as a supported authentication method (required for organization invites).
    • Optionally allow Google or other authentication methods.
  • Add Environment Variables: Copy the NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY from the Clerk dashboard to the .env.local file.
  • Install Clerk Next.js Package: Use npm install @clerk/nextjs.
  • Create a Middleware File: Create middleware.ts to protect the application with authentication.
    • Copy the middleware code from the Clerk documentation.
  • Create a JWT Template:
    • Go to "JWT Templates" in the Clerk dashboard.
    • Create a new template for "Convex."
    • Ensure the "AUD" claim is set to "convex."
    • Add "organization role" and "organization ID" to the JWT template.
    • Copy the "Issuer URL" from the JWT template.
  • Configure Convex Authentication:
    • Create a file named auth.config.js inside the convex folder.
    • Copy the code from the Convex documentation for Clerk authentication.
    • Replace the dummy domain with the "Issuer URL" from the Clerk JWT template.

9. Creating a Universal Provider (Convex and Clerk):

  • Create a Providers Folder: Create a folder named providers in the root of the application.
  • Create a Client Provider: Create a file named convex-client-provider.tsx inside the providers folder.
  • Mark as Client Component: Add "use client" at the top of the file.
  • Import Necessary Modules:
    • ClerkProvider and useAuth from @clerk/nextjs.
    • ConvexProviderWithClerk, useAuth, AuthLoading, Authenticated, and ConvexReactClient from convex/react-clerk.
  • Create a Convex Client:
    • Get the Convex URL from process.env.NEXT_PUBLIC_CONVEX_URL.
    • Create a new ConvexReactClient instance with the Convex URL.
  • Create a Provider Component:
    • Create a React component named ConvexClientProvider that accepts a children prop.
    • Wrap the children with ClerkProvider and ConvexProviderWithClerk.
    • Pass useAuth to ConvexProviderWithClerk as the useAuth prop.
    • Pass the Convex client instance to ConvexProviderWithClerk as the client prop.
  • Wrap the Application:
    • Go to app/layout.tsx.
    • Wrap the children prop inside the body tag with the ConvexClientProvider component.

10. Adding a Loading State and Logout:

  • Create a Loading Component:
    • Create a folder named out inside the components folder.
    • Create a file named loading.tsx inside the out folder.
    • Create a React component that displays a loading indicator (e.g., a logo with a pulsing animation).
  • Modify the Client Provider:
    • In convex-client-provider.tsx, use the AuthLoading and Authenticated components from convex/react-clerk.
    • Render the loading component when AuthLoading is true.
    • Render the children prop only when Authenticated is true.
  • Add a User Button:
    • In app/page.tsx, add the UserButton component from @clerk/nextjs.
    • This component provides a user interface for managing the user's account and logging out.

11. Project Layout and Structure:

  • Dashboard Route Group: Move the root page.tsx inside a route group folder named dashboard (e.g., app/dashboard/page.tsx).
  • Dashboard Layout: Create a layout.tsx file inside the dashboard folder to define a reusable layout for the dashboard.
  • Sidebar Component: Create a sidebar component inside the components folder.
  • Organization Sidebar Component: Create an organization-sidebar component inside the components folder.
  • Navbar Component: Create a navbar component inside the components folder.
  • Layout Structure:
    • Use a main element to wrap the entire layout.
    • Render the sidebar component inside the main element.
    • Use a flex container to position the organization-sidebar, navbar, and content area.

12. Implementing Organization Management:

  • Enable Organizations in Clerk: Go to the Clerk dashboard, select the application, and enable organizations.
  • Update JWT Template: Add "organization role" and "organization ID" to the Convex JWT template in Clerk.
  • Install Dialogue and Tooltip Components: Use npx shadcn-ui@latest add dialog tooltip.
  • New Button Component: Create a new-button component inside the sidebar folder to trigger the organization creation dialogue.
  • List Component: Create a list component inside the sidebar folder to display a list of organizations.
  • Item Component: Create an item component inside the sidebar folder to represent an individual organization in the list.
  • Hint Component: Create a reusable hint component to display tooltips on hover.
  • Organization Switcher: Use the OrganizationSwitcher component from @clerk/nextjs to allow users to switch between organizations.
  • Invitation Functionality: Use the Clerk organization features to invite team members to organizations.

13. Implementing Search and Favorite Boards:

  • Add Search Input: Create a search-input component to allow users to search for boards.
  • Use Query String Package: Install query-string and use-hooks-ts for managing URL parameters.
  • Implement Search Logic: Use useState, useDebounce, and useRouter to update the URL with the search query.
  • Create Empty States: Create separate components for empty search results, empty favorite boards, and empty board lists.
  • Board List Component: Create a board-list component to display a list of boards, handle empty states, and pass search parameters.
  • Board Card Component: Create a board-card component to represent an individual board in the list.
  • Favorite Boards: Add a "favorites" query parameter to the URL to filter the board list.
  • Dynamic Variant: Use the useSearchParams hook to dynamically change the variant of the "Team boards" and "Favorite boards" buttons based on the URL.

14. Implementing Board Actions (Rename, Delete, Copy Link):

  • Install Dropdown Menu and Alert Dialogue Components: Use npx shadcn-ui@latest add dropdown-menu alert-dialog.
  • Create Actions Component: Create a reusable actions component to display a dropdown menu with options to copy the board link, delete the board, and rename the board.
  • Copy Board Link: Implement the navigator.clipboard.writeText API to copy the board link to the clipboard.
  • Delete Board: Create a Convex mutation to delete a board from the database.
  • Confirm Model Component: Create a reusable confirm-model component to display a confirmation dialogue before deleting a board.
  • Rename Board: Create a Convex mutation to update the title of a board.
  • Use Rename Model Hook: Create a useRenameModel hook using zustand to manage the state of the rename dialogue.

15. Implementing Favoriting Functionality:

  • Extend Database Schema: Add a userFavorites table to the Convex schema to store user-board relationships.
  • Create Favorite and Unfavorite Mutations: Create Convex mutations to add and remove boards from the user's favorites.
  • Modify Board Card Component: Add a button to the board-card component to toggle the favorite status of a board.
  • Update Board Query: Modify the Convex query to include a boolean field indicating whether a board is favorited by the current user.
  • Dynamic Class Name: Use a dynamic class name to change the appearance of the favorite button based on the board's favorite status.

16. Implementing Search and Favorite Queries:

  • Modify Convex Boards Function: Add optional search and favorites parameters to the Convex query for boards.
  • Implement Search Index: Use the Convex search index to filter boards based on the search query.
  • Implement Favorite Query: Use the userFavorites table to filter boards based on the user's favorite boards.
  • Conditional Rendering: Conditionally render the appropriate empty state component based on the search query and favorite status.

17. Implementing Board Creation and Redirection:

  • Modify Empty Boards Component: Add a button to the empty-boards component to create a new board.
  • Use Router Hook: Use the useRouter hook from next/navigation to redirect the user to the newly created board.
  • Modify New Board Button Component: Add the same redirection logic to the new-board-button component.

18. Creating the Board View (Canvas):

  • Create a Board Route: Create a new folder named board in the app folder, and inside create a dynamic route segment [boardId] with a page.tsx file.
  • Create Canvas Component: Create a canvas component to represent the whiteboard canvas.
  • Create Info Component: Create an info component to display information about the board.
  • Create Participants Component: Create a participants component to display a list of users in the board.
  • Create Toolbar Component: Create a toolbar component to display the drawing tools and actions.
  • Basic Layout: Position the info, participants, and toolbar components around the canvas component.

19. Connecting to Liveblocks:

  • Install Liveblocks Packages: Use npm install @liveblocks/client @liveblocks/react.
  • Initialize Liveblocks: Use npx create-liveblocks-app@latest --init --framework react to create a liveblocks.config.ts file.
  • Configure Liveblocks:
    • Replace the public API key with the one from the Liveblocks dashboard.
    • Add the authentication endpoint.
  • Create a Room Component: Create a room component to wrap the canvas component and provide realtime collaboration features.
  • Room Provider: Use the RoomProvider component from @liveblocks/react to connect to a Liveblocks room.
  • Client-Side Suspense: Use the ClientSideSuspense component from @liveblocks/react to handle loading states.

20. Implementing Authentication with Liveblocks:

  • Install Liveblocks Node Package: Use npm install @liveblocks/node.
  • Create Authentication Endpoint: Create a route handler at app/api/liveblocks-auth/route.ts to authenticate users.
  • Initialize Liveblocks and Convex: Initialize Liveblocks and Convex instances in the route handler.
  • Authenticate User: Use Clerk's auth and currentUser functions to authenticate the user.
  • Check Organization Membership: Verify that the user is a member of the organization associated with the board.
  • Prepare Session: Use liveblocks.prepareSession to create a Liveblocks session.
  • Allow Room Access: Use session.allowRoom to grant the user access to the Liveblocks room.
  • Configure Liveblocks Client: Update the liveblocks.config.ts file to use the authentication endpoint.

21. Implementing Realtime Cursors:

  • Extend Liveblocks Configuration: Add cursor to the presence type in liveblocks.config.ts.
  • Create Cursors Presence Component: Create a cursors-presence component to render other users' cursors.
  • Use UseOthers Hook: Use the useOthers hook from @liveblocks/react to get a list of other users in the room.
  • Create Cursor Component: Create a cursor component to represent an individual user's cursor.
  • Render Foreign Object: Use a foreignObject element to render the cursor icon and user's name inside the SVG canvas.
  • Update Presence: Use the useMutation hook to update the user's presence with their cursor coordinates.
  • Pointer Event to Canvas Point: Create a utility function to convert pointer event coordinates to canvas coordinates.
  • On Pointer Move Event: Add an onPointerMove event handler to the SVG canvas to update the user's presence with their cursor coordinates.
  • On Pointer Leave Event: Add an onPointerLeave event handler to the SVG canvas to reset the user's cursor presence when they leave the canvas.

22. Implementing Drawing Functionality (Pencil Tool):

  • Extend Liveblocks Configuration: Add pencilDraft and penColor to the presence type in liveblocks.config.ts.
  • Create Start Drawing Mutation: Create a Convex mutation to initialize the pencil draft with the starting point.
  • Create Continue Drawing Mutation: Create a Convex mutation to update the pencil draft with new points.
  • Create Insert Path Mutation: Create a Convex mutation to create a new path layer from the pencil draft.
  • Create Penpoints to Path Layer Util: Create a utility function to convert an array of points to a path layer.
  • Create Path Component: Create a path component to render the path layer using the getStroke function from perfect-freehand.
  • Update On Pointer Down Event: Add a case to the onPointerDown event handler to initialize the pencil draft when the pencil tool is selected.
  • Update On Pointer Move Event: Add a case to the onPointerMove event handler to update the pencil draft with new points.
  • Update On Pointer Up Event: Add a case to the onPointerUp event handler to create a new path layer from the pencil draft.
  • Render Drafts: Render the pencil drafts of other users in the cursors-presence component.

23. Implementing Basic Shape Functionality (Rectangle, Ellipse, Text, Note):

  • Create Layer Types: Define enums for the different layer types (rectangle, ellipse, text, note).
  • Create Layer Components: Create React components for each layer type (rectangle, ellipse, text, note).
  • Update Layer Preview Component: Add cases to the layer-preview component to render the appropriate component for each layer type.
  • Create Insert Layer Mutation: Create a Convex mutation to insert a new layer into the database.
  • Update On Pointer Up Event: Add a case to the onPointerUp event handler to call the insertLayer mutation when a shape tool is selected.

24. Implementing Selection and Transformation:

  • Create Use Selection Bounds Hook: Create a hook to calculate the bounding box of the selected layers.
  • Create Selection Box Component: Create a selection-box component to display a selection box around the selected layers.
  • Implement Resizing Functionality:
    • Add onResizeHandlePointerDown event to the selection box handles.
    • Create a resizeBounds utility function to calculate the new bounds of the selected layers.
    • Create a resizeSelectedLayer mutation to update the dimensions of the selected layers.
  • Implement Translating Functionality:
    • Add a translateSelectedLayers mutation to update the position of the selected layers.
    • Update the onPointerMove event handler to call the translateSelectedLayers mutation when the user is dragging the selected layers.
  • Implement Deselection Functionality:
    • Add an onPointerDown event handler to the SVG canvas to deselect the layers when the user clicks outside of the selected layers.
    • Create an unselectLayers mutation to clear the selection.

25. Additional Tips and Enhancements:

  • Use Disable Scroll Bounce Hook: Create a hook to disable scroll bouncing on iOS devices.
  • Implement Keyboard Shortcuts: Add keyboard shortcuts for common actions (e.g., Ctrl+Z for undo, Ctrl+Shift+Z for redo, Backspace for delete).
  • Replace Two Array with Two Immutable: Refactor the code to use twoImmutable instead of twoArray (recommended by the Liveblocks team).

26. Deployment to Vercel:

  • Build the Project: Use npm run build to build the project for production.
  • Create a GitHub Repository: Create a new repository on GitHub.
  • Push the Code to GitHub: Push the local code to the new GitHub repository.
  • Deploy to Vercel:
    • Go to vercel.com/new.
    • Select the GitHub repository.
    • Override the build command with npx convex deploy --cmd npm run build.
    • Add environment variables from .env.local to Vercel.
    • Generate a production deploy key in the Convex dashboard and add it to Vercel.
    • Deploy the application.

27. Conclusion:

  • The tutorial provides a comprehensive guide to building a realtime collaborative whiteboard application with Next.js 14, Convex, Clerk, and Liveblocks.
  • It covers a wide range of topics, including setting up the project, implementing realtime features, managing user authentication, and deploying the application.
  • The resulting application is a powerful tool for brainstorming, planning, and team collaboration.

AI summaries can miss context or contain errors. Check important details against the original video.

Go a little deeper.

Have a question about this video? Load its transcript to open the video chat.