Build a Notion/Slack-style team workspace with V0 featuring organizations, channels with real-time chat, wiki pages, member management, and invite links. You'll create multi-tenant architecture with RLS, Supabase Realtime for live messaging, and Clerk authentication — all in about 1-2 hours.
What you're building
Teams need a shared space to communicate, document knowledge, and coordinate work. Slack handles chat, Notion handles docs, but a unified workspace combines both — channels for real-time discussion and pages for persistent documentation.
V0 generates the workspace layout, channel chat, wiki editor, and member management from prompts. Supabase provides the database with Realtime for live messaging. Clerk handles authentication with pre-built UI components for the fastest setup.
The architecture uses dynamic routes at app/[org]/ for multi-tenant organization scoping, Supabase Realtime for live channel messages, Server Components for wiki pages, and RLS policies joined through org_members to ensure every query is organization-scoped.
Final result
A team workspace with organization management, real-time channel messaging, wiki pages with nesting, role-based member management, and invite links.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Clerk account (free tier includes 10,000 monthly active users)
- Basic understanding of workspaces and channels (like Slack or Discord)
Build steps
Set up the multi-tenant database schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the organizations, org_members, channels, messages, and pages tables with multi-tenant RLS policies.
1// Paste this prompt into V0's AI chat:2// Create a Supabase schema for a team workspace:3// 1. organizations table: id (uuid PK), name (text), slug (text UNIQUE), logo_url (text), created_at (timestamptz)4// 2. org_members table: org_id (uuid FK), user_id (uuid FK), role (text — 'owner', 'admin', 'member'), joined_at (timestamptz), PRIMARY KEY(org_id, user_id)5// 3. channels table: id (uuid PK), org_id (uuid FK), name (text), description (text), is_private (boolean DEFAULT false), created_at (timestamptz)6// 4. messages table: id (uuid PK), channel_id (uuid FK), author_id (uuid FK), content (text), edited_at (timestamptz nullable), created_at (timestamptz)7// 5. pages table: id (uuid PK), org_id (uuid FK), title (text), content (text — Markdown), author_id (uuid FK), parent_page_id (uuid FK nullable for nesting), updated_at (timestamptz)8// RLS: all tables scoped through org_members — users must be a member to access any org data.9// Enable Realtime on messages table.10// Seed a sample org with 3 channels and 5 pages.Pro tip: Store NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY in V0's Vars tab. The publishable key gets the NEXT_PUBLIC_ prefix for client-side auth, the secret key has no prefix for server-only use.
Expected result: Five tables created with multi-tenant RLS policies, Realtime enabled on messages, and sample workspace data seeded.
Build the workspace layout with sidebar navigation
Create the workspace layout with a sidebar showing channels and pages as a tree. The layout uses dynamic [org] routing for multi-tenant URL structure.
1import { createClient } from '@/lib/supabase/server'2import { ScrollArea } from '@/components/ui/scroll-area'3import { Button } from '@/components/ui/button'4import { Separator } from '@/components/ui/separator'5import Link from 'next/link'6import { notFound } from 'next/navigation'78export default async function WorkspaceLayout({9 children,10 params,11}: {12 children: React.ReactNode13 params: Promise<{ org: string }>14}) {15 const { org } = await params16 const supabase = await createClient()1718 const { data: organization } = await supabase19 .from('organizations')20 .select('*')21 .eq('slug', org)22 .single()2324 if (!organization) notFound()2526 const { data: channels } = await supabase27 .from('channels')28 .select('id, name')29 .eq('org_id', organization.id)30 .order('name')3132 const { data: pages } = await supabase33 .from('pages')34 .select('id, title, parent_page_id')35 .eq('org_id', organization.id)36 .is('parent_page_id', null)3738 return (39 <div className="flex h-screen">40 <aside className="w-64 border-r bg-muted/30">41 <div className="p-4">42 <h2 className="font-bold text-lg">{organization.name}</h2>43 </div>44 <Separator />45 <ScrollArea className="flex-1 p-2">46 <p className="text-xs font-semibold text-muted-foreground px-2 mb-1">Channels</p>47 {channels?.map((ch) => (48 <Link key={ch.id} href={`/${org}/channels/${ch.id}`}>49 <Button variant="ghost" size="sm" className="w-full justify-start">50 # {ch.name}51 </Button>52 </Link>53 ))}54 <Separator className="my-2" />55 <p className="text-xs font-semibold text-muted-foreground px-2 mb-1">Pages</p>56 {pages?.map((page) => (57 <Link key={page.id} href={`/${org}/pages/${page.id}`}>58 <Button variant="ghost" size="sm" className="w-full justify-start">59 {page.title}60 </Button>61 </Link>62 ))}63 </ScrollArea>64 </aside>65 <main className="flex-1 overflow-auto">{children}</main>66 </div>67 )68}Expected result: A workspace layout with a sidebar showing channels (prefixed with #) and pages as a tree. The main content area renders channel or page content.
Create the real-time channel messaging view
Build a client component for channel messaging that subscribes to Supabase Realtime for instant message delivery. Messages display in Card components with Avatar and timestamp.
1'use client'23import { useEffect, useState } from 'react'4import { createClient } from '@/lib/supabase/client'5import { Card, CardContent } from '@/components/ui/card'6import { Avatar, AvatarFallback } from '@/components/ui/avatar'7import { Textarea } from '@/components/ui/textarea'8import { Button } from '@/components/ui/button'9import { sendMessage } from '@/app/actions/workspace'1011type Message = {12 id: string13 content: string14 author_id: string15 created_at: string16}1718export function ChannelChat({ channelId, initialMessages }: {19 channelId: string20 initialMessages: Message[]21}) {22 const [messages, setMessages] = useState(initialMessages)23 const supabase = createClient()2425 useEffect(() => {26 const channel = supabase27 .channel(`channel-${channelId}`)28 .on('postgres_changes', {29 event: 'INSERT',30 schema: 'public',31 table: 'messages',32 filter: `channel_id=eq.${channelId}`,33 }, (payload) => {34 setMessages((prev) => [...prev, payload.new as Message])35 })36 .subscribe()37 return () => { supabase.removeChannel(channel) }38 }, [supabase, channelId])3940 return (41 <div className="flex flex-col h-full">42 <div className="flex-1 overflow-y-auto p-4 space-y-3">43 {messages.map((msg) => (44 <div key={msg.id} className="flex gap-3">45 <Avatar className="w-8 h-8">46 <AvatarFallback>U</AvatarFallback>47 </Avatar>48 <div>49 <p className="text-sm">{msg.content}</p>50 <p className="text-xs text-muted-foreground">51 {new Date(msg.created_at).toLocaleTimeString()}52 </p>53 </div>54 </div>55 ))}56 </div>57 <form action={sendMessage} className="p-4 border-t flex gap-2">58 <input type="hidden" name="channelId" value={channelId} />59 <Textarea name="content" placeholder="Type a message..." rows={1} className="flex-1" />60 <Button type="submit">Send</Button>61 </form>62 </div>63 )64}Expected result: Real-time channel chat where messages appear instantly for all connected users. Each message shows an Avatar, content, and timestamp.
Build member management with invite links
Create an organization settings page with a member Table showing roles, a Select for changing roles, and a Dialog for generating invite links.
1// Paste this prompt into V0's AI chat:2// Build an organization settings page at app/[org]/settings/page.tsx with:3// 1. Member management Table showing: Avatar, display name, email, role Select (owner/admin/member), joined date, Remove Button4// 2. "Invite Member" Button that opens a Dialog:5// - Generate a unique invite link using crypto.randomUUID()6// - Input field showing the link with a Copy button7// - Optional: Input for email to send invite directly8// 3. Server Actions for: change role, remove member, create invite, accept invite9// 4. AlertDialog for confirming member removal10// 5. Organization details section: Input for org name, org logo upload area11// 6. Use Tabs for Members and Settings sections12// RLS ensures only admin+ roles can modify members.Pro tip: Multi-tenant RLS is the biggest challenge — every query must be scoped to the current org. Create a Supabase RPC function that validates org membership before returning data.
Expected result: A settings page with member Table, role management Select, invite link Dialog, and organization details editor.
Add wiki pages with nested hierarchy
Create the wiki page view and editor with Markdown content, nested page hierarchy using parent_page_id, and Breadcrumb navigation showing the page path.
1// Paste this prompt into V0's AI chat:2// Build a wiki page system for the team workspace:3// 1. Page view at app/[org]/pages/[id]/page.tsx:4// - Server Component fetching the page from Supabase5// - Breadcrumb navigation showing the page hierarchy (root > parent > current)6// - Markdown content rendered in prose styling7// - Edit Button, New Child Page Button8// - Sidebar showing child pages as links9// 2. Page editor at app/[org]/pages/[id]/edit/page.tsx:10// - Input for title11// - Textarea for Markdown content (full-width)12// - Select for parent page (to reorganize hierarchy)13// - Save Button calling a Server Action14// 3. Server Actions for: create page, update page, delete page, move page (change parent)15// Use Breadcrumb, Input, Textarea, Select, Button from shadcn/ui.Expected result: Wiki pages with Breadcrumb navigation, Markdown content, child page listing, and an editor for creating and modifying pages.
Complete code
1import { createClient } from '@/lib/supabase/server'2import { ScrollArea } from '@/components/ui/scroll-area'3import { Button } from '@/components/ui/button'4import { Separator } from '@/components/ui/separator'5import Link from 'next/link'6import { notFound } from 'next/navigation'78export default async function WorkspaceLayout({9 children,10 params,11}: {12 children: React.ReactNode13 params: Promise<{ org: string }>14}) {15 const { org } = await params16 const supabase = await createClient()1718 const { data: organization } = await supabase19 .from('organizations')20 .select('*')21 .eq('slug', org)22 .single()2324 if (!organization) notFound()2526 const { data: channels } = await supabase27 .from('channels')28 .select('id, name')29 .eq('org_id', organization.id)30 .order('name')3132 const { data: pages } = await supabase33 .from('pages')34 .select('id, title')35 .eq('org_id', organization.id)36 .is('parent_page_id', null)3738 return (39 <div className="flex h-screen">40 <aside className="w-64 border-r bg-muted/30 flex flex-col">41 <div className="p-4">42 <h2 className="font-bold">{organization.name}</h2>43 </div>44 <Separator />45 <ScrollArea className="flex-1 p-2">46 <p className="text-xs font-semibold text-muted-foreground px-2 mb-1">47 Channels48 </p>49 {channels?.map((ch) => (50 <Link key={ch.id} href={`/${org}/channels/${ch.id}`}>51 <Button variant="ghost" size="sm" className="w-full justify-start">52 # {ch.name}53 </Button>54 </Link>55 ))}56 <Separator className="my-2" />57 <p className="text-xs font-semibold text-muted-foreground px-2 mb-1">58 Pages59 </p>60 {pages?.map((page) => (61 <Link key={page.id} href={`/${org}/pages/${page.id}`}>62 <Button variant="ghost" size="sm" className="w-full justify-start">63 {page.title}64 </Button>65 </Link>66 ))}67 </ScrollArea>68 </aside>69 <main className="flex-1 overflow-auto">{children}</main>70 </div>71 )72}Customization ideas
Add file sharing in channels
Use Supabase Storage to let users upload and share files in channel messages. Display file attachments as downloadable links or image previews.
Add threaded replies
Add a parent_message_id to the messages table for threaded conversations. Show reply counts on parent messages and open threads in a side panel.
Add @mentions and notifications
Parse @username mentions in messages, store them in a mentions table, and send in-app or email notifications to mentioned users.
Add page version history
Create a page_versions table that stores every edit. Add a version history Dialog showing diffs between versions with restore functionality.
Common pitfalls
Pitfall: Not scoping all queries to the current organization
How to avoid: Every Supabase query must include .eq('org_id', currentOrgId) or use RLS policies that join through org_members to verify membership.
Pitfall: Not enabling Supabase Realtime on the messages table
How to avoid: Enable Realtime on the messages table in Supabase Dashboard > Database > Replication.
Pitfall: Using NEXT_PUBLIC_ prefix for CLERK_SECRET_KEY
How to avoid: Store CLERK_SECRET_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix. Only NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY should have the prefix.
Best practices
- Use multi-tenant RLS policies on all tables — every query must be scoped to the current organization via org_members membership
- Store NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (client) and CLERK_SECRET_KEY (server-only, no prefix) in V0's Vars tab
- Enable Supabase Realtime on the messages table for live chat without polling
- Use dynamic [org] routes for multi-tenant URL structure so each workspace has a clean URL like /acme/channels/general
- Use Server Components for wiki pages and member management — they load faster and don't need real-time updates
- Use Design Mode (Option+D) to visually adjust sidebar width, channel list spacing, and message bubble styling at zero credit cost
- Use Breadcrumb for wiki page hierarchy navigation so users always know where they are in the page tree
AI prompts to try
Copy these prompts to build this project faster.
I'm building a team workspace with Next.js App Router, Supabase, and Clerk. I need: 1) Multi-tenant organization structure with dynamic [org] routes, 2) Real-time channel messaging with Supabase Realtime, 3) Wiki pages with nested hierarchy, 4) Member management with role-based access. Help me design multi-tenant RLS policies that scope all data to the current organization.
Design multi-tenant RLS policies for a team workspace with Supabase. Tables: organizations, org_members, channels, messages, pages. Requirements: 1) Users can only access data from organizations they belong to, 2) Only admins and owners can modify org settings and members, 3) All members can read channels and pages, 4) Members can only edit their own messages. Create a helper RPC function check_org_membership(user_id, org_id) that returns boolean, and use it in all RLS policies.
Frequently asked questions
How does multi-tenant isolation work?
Every table has an org_id column. RLS policies on all tables check that the current user is a member of the organization via the org_members table. Even if someone guesses a channel or page UUID, they cannot access it without being an org member.
What V0 plan do I need?
V0 Free tier works. The workspace uses standard Server Components, client components for chat, and shadcn/ui. Supabase Realtime is included on the free tier. Clerk free tier includes 10,000 monthly active users.
Can I use Supabase Auth instead of Clerk?
Yes. Replace Clerk with Supabase Auth by using createBrowserClient and createServerClient for auth. Clerk is recommended for fastest setup with pre-built UI components, but Supabase Auth works if you are already using Supabase.
How do real-time messages work?
Supabase Realtime pushes new message INSERT events via WebSocket to all clients subscribed to that channel. The client component prepends new messages to the local state without any refresh.
How do I deploy this workspace?
Click Share > Publish in V0. Add CLERK_SECRET_KEY in the Vercel Dashboard. The Supabase connection is auto-configured from the Connect panel. Make sure Realtime is enabled on the messages table.
Can RapidDev help build a custom team workspace?
Yes. RapidDev has built 600+ apps including team collaboration platforms with real-time messaging, document collaboration, and custom integrations. Book a free consultation to discuss your workspace requirements.
Can I add file sharing to channels?
Yes. Use Supabase Storage to create a bucket for workspace files. Add a file upload Input to the message composer. Upload the file, get the public URL, and include it in the message content.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation