# How to Build Admin panel with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build a role-based admin panel with V0 using Next.js, Supabase, and Clerk authentication. You'll get a dashboard with user management, audit logging, and app settings — complete with role guards, optimistic UI updates, and a polished sidebar layout in about 1-2 hours.

## Before you start

- A V0 account (Premium plan recommended for multiple prompts)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Clerk account (free tier supports up to 10,000 monthly active users)
- Basic understanding of user roles (admin, editor, viewer)

## Step-by-step guide

### 1. Set up the project with Clerk auth and Supabase database

Create a new V0 project and connect both Clerk and Supabase via the Connect panel. Clerk handles authentication and provides the user session, while Supabase stores your application data including profiles, audit logs, and settings.

```
// Paste this prompt into V0's AI chat:
// Build an admin panel with Clerk authentication and Supabase backend.
// Create a Supabase schema with these tables:
// 1. profiles: id (uuid PK), email (text), role (text CHECK in 'admin','editor','viewer'), full_name (text), avatar_url (text), created_at (timestamptz)
// 2. audit_logs: id (uuid PK), user_id (uuid FK), action (text), entity_type (text), entity_id (uuid), metadata (jsonb), created_at (timestamptz)
// 3. settings: id (uuid PK), key (text unique), value (jsonb), updated_by (uuid FK), updated_at (timestamptz)
// Add RLS policies that restrict access based on the role column in profiles.
// Generate the SQL migration.
```

> Pro tip: Use V0's prompt queuing — queue the schema prompt first, then immediately queue the layout prompt below. V0 processes them sequentially so the layout can reference the tables.

**Expected result:** Supabase is connected with tables created. Clerk is connected with CLERK_SECRET_KEY and NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY auto-provisioned in the Vars tab.

### 2. Build the admin layout with sidebar navigation and role guard

Create the main admin layout that checks the user's role before rendering any admin pages. Non-admin users see an access denied message. The sidebar provides navigation between users, audit logs, and settings.

```
import { auth, currentUser } from '@clerk/nextjs/server'
import { createClient } from '@supabase/supabase-js'
import { redirect } from 'next/navigation'
import { AdminSidebar } from '@/components/admin-sidebar'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const { userId } = await auth()
  if (!userId) redirect('/sign-in')

  const { data: profile } = await supabase
    .from('profiles')
    .select('role, full_name, avatar_url')
    .eq('id', userId)
    .single()

  if (!profile || profile.role === 'viewer') {
    redirect('/unauthorized')
  }

  return (
    <div className="flex h-screen">
      <AdminSidebar
        role={profile.role}
        name={profile.full_name}
        avatar={profile.avatar_url}
      />
      <main className="flex-1 overflow-y-auto p-6">{children}</main>
    </div>
  )
}
```

**Expected result:** The admin layout renders a sidebar on the left and page content on the right. Users without admin or editor roles are redirected to /unauthorized.

### 3. Create the user management page with DataTable and role editing

Build the users page that displays all profiles in a sortable, filterable table. Admins can change user roles inline with a Select dropdown that uses optimistic updates for instant feedback.

```
import { createClient } from '@supabase/supabase-js'
import { UserTable } from '@/components/user-table'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export default async function UsersPage() {
  const { data: users } = await supabase
    .from('profiles')
    .select('*')
    .order('created_at', { ascending: false })

  const totalUsers = users?.length ?? 0
  const admins = users?.filter((u) => u.role === 'admin').length ?? 0

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">User Management</h1>
      <div className="grid gap-4 md:grid-cols-3">
        <Card>
          <CardHeader><CardTitle>Total Users</CardTitle></CardHeader>
          <CardContent className="text-3xl font-bold">{totalUsers}</CardContent>
        </Card>
        <Card>
          <CardHeader><CardTitle>Admins</CardTitle></CardHeader>
          <CardContent className="text-3xl font-bold">{admins}</CardContent>
        </Card>
      </div>
      <UserTable users={users ?? []} />
    </div>
  )
}
```

> Pro tip: Use Design Mode (Option+D) to visually adjust the stat Card spacing, table column widths, and role Badge colors without spending any credits.

### 4. Add the audit log page for tracking admin actions

Create the audit log page that shows every admin action — who did what, when, and to which entity. This is critical for compliance and debugging. The Server Action that updates roles automatically inserts an audit log entry.

```
// Paste this prompt into V0's AI chat:
// Build an audit log page at app/admin/audit/page.tsx.
// Requirements:
// - Fetch all audit_logs from Supabase joined with profiles for the user name
// - Display in a shadcn/ui Table with columns: Date, User, Action, Entity Type, Entity ID
// - Add Badge for action types (color-coded: create=green, update=blue, delete=red)
// - Add a Select filter for action type and a DatePicker for date range
// - Show the metadata jsonb in an expandable row detail using Collapsible
// - Paginate with 25 items per page using cursor-based pagination on created_at
// - Use Server Components for data fetching, no 'use client' unless needed for filters
```

**Expected result:** The audit log page shows a chronological list of admin actions with filters for action type and date range. Each row is expandable to show metadata details.

### 5. Build the settings page with Server Actions for safe updates

Create a settings page where admins can update application configuration like site name, maintenance mode, and feature flags. Use Server Actions for mutations so settings changes are validated server-side and logged to the audit trail.

```
'use server'

import { createClient } from '@supabase/supabase-js'
import { auth } from '@clerk/nextjs/server'
import { revalidatePath } from 'next/cache'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function updateSetting(key: string, value: unknown) {
  const { userId } = await auth()
  if (!userId) throw new Error('Unauthorized')

  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', userId)
    .single()

  if (profile?.role !== 'admin') throw new Error('Forbidden')

  const { error } = await supabase
    .from('settings')
    .upsert({ key, value, updated_by: userId, updated_at: new Date().toISOString() })

  if (error) throw new Error(error.message)

  await supabase.from('audit_logs').insert({
    user_id: userId,
    action: 'update',
    entity_type: 'setting',
    metadata: { key, value },
  })

  revalidatePath('/admin/settings')
}
```

> Pro tip: Every Server Action that modifies data should insert an audit_logs row. This creates a full paper trail of who changed what and when — essential for any serious admin panel.

**Expected result:** The settings page displays current configuration values. Admins can edit values inline, and every change is saved to Supabase and recorded in the audit log.

## Complete code example

File: `app/admin/layout.tsx`

```typescript
import { auth } from '@clerk/nextjs/server'
import { createClient } from '@supabase/supabase-js'
import { redirect } from 'next/navigation'
import { SidebarProvider, Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuButton } from '@/components/ui/sidebar'
import { Users, Shield, Settings, FileText } from 'lucide-react'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const navItems = [
  { title: 'Users', href: '/admin/users', icon: Users },
  { title: 'Audit Log', href: '/admin/audit', icon: FileText },
  { title: 'Settings', href: '/admin/settings', icon: Settings },
]

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const { userId } = await auth()
  if (!userId) redirect('/sign-in')

  const { data: profile } = await supabase
    .from('profiles')
    .select('role, full_name')
    .eq('id', userId)
    .single()

  if (!profile || profile.role === 'viewer') {
    redirect('/unauthorized')
  }

  return (
    <SidebarProvider>
      <div className="flex h-screen w-full">
        <Sidebar>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Admin Panel</SidebarGroupLabel>
              <SidebarMenu>
                {navItems.map((item) => (
                  <SidebarMenuItem key={item.href}>
                    <SidebarMenuButton asChild>
                      <a href={item.href}>
                        <item.icon className="h-4 w-4" />
                        <span>{item.title}</span>
                      </a>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
        <main className="flex-1 overflow-y-auto p-6">
          {children}
        </main>
      </div>
    </SidebarProvider>
  )
}
```

## Common mistakes

- **Not implementing role checks on the server side** — Hiding UI elements is not security. Users can still call Server Actions or API routes directly if server-side role validation is missing. Fix: Always verify the user's role in every Server Action and API route by querying the profiles table. Never rely solely on client-side role checks.
- **Using NEXT_PUBLIC_ prefix for CLERK_SECRET_KEY** — Beginners add NEXT_PUBLIC_ to all env vars, but this exposes the secret key in the browser bundle, creating a critical security vulnerability. Fix: Store CLERK_SECRET_KEY in V0's Vars tab without any prefix. Only NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY should have the NEXT_PUBLIC_ prefix.
- **Forgetting to log admin actions to the audit trail** — Without audit logs, you cannot track who made changes or debug issues. This is also a compliance requirement for many industries. Fix: Insert an audit_logs row in every Server Action that modifies data. Include the user_id, action type, entity type, and metadata about what changed.
- **Not handling optimistic update rollbacks** — If the Server Action fails after the UI has already updated, the user sees stale data that does not match the database. Fix: Use React's useOptimistic hook to update the UI immediately on role changes. If the Server Action throws an error, the optimistic state automatically reverts.

## Best practices

- Use Server Components by default for all admin pages — they fetch data directly from Supabase without exposing your service role key to the browser
- Store CLERK_SECRET_KEY and SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix to keep them server-only
- Use Design Mode (Option+D) to visually adjust sidebar width, table spacing, and Badge colors without spending credits
- Add RLS policies on every Supabase table scoped by the role column in the profiles table for defense-in-depth security
- Use revalidatePath() in Server Actions after mutations to ensure the UI reflects the latest data without manual refreshing
- Implement cursor-based pagination for the audit log table to handle large datasets efficiently
- Use shadcn/ui AlertDialog for all destructive actions (delete user, revoke access) to prevent accidental clicks
- Set up a Clerk webhook to automatically create a profiles row when new users sign up, keeping Clerk and Supabase in sync

## Frequently asked questions

### What is the best authentication provider for a V0 admin panel?

Clerk is the fastest option because it provides pre-built sign-in components, role management, and webhook sync. The free tier supports 10,000 monthly active users, which is more than enough for most admin panels. Supabase Auth is a good alternative if you want everything in one platform.

### How do I restrict admin pages to specific user roles?

Add a role check in your admin layout.tsx using a Server Component. Query the user's profile from Supabase after authenticating with Clerk, and redirect to an unauthorized page if their role is not admin or editor. This runs server-side, so the page never renders for unauthorized users.

### Can I use the V0 free plan to build an admin panel?

You can start on the free plan, but the admin panel requires multiple features (layout, user table, audit log, settings) that will use several prompts. V0 Premium gives you more credits and faster generation, which is better suited for multi-page projects like this.

### How do I deploy the admin panel to production?

Click Share then Publish to Production in V0 — it deploys to Vercel in 30-60 seconds. Alternatively, connect a GitHub repo via the Git panel, and V0 creates a branch with a pull request. Merge the PR to trigger an automatic Vercel deployment.

### How do I handle audit logging without slowing down the admin panel?

Insert audit log entries in the same Server Action that performs the mutation, but do not await the audit insert if speed is critical — use a fire-and-forget pattern. For most admin panels, the few milliseconds added by the audit insert are negligible.

### Can I add multiple admin roles with different permissions?

Yes. The profiles table uses a role column with values like admin, editor, and viewer. You can add more roles and check them in your Server Actions and layouts. For fine-grained permissions, consider a separate permissions table with resource-level access controls.

### Can RapidDev help build a custom admin panel?

Yes. RapidDev has built 600+ apps including complex admin dashboards with multi-tenant role systems, audit compliance, and custom analytics. Book a free consultation to discuss your specific admin panel requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/admin-panel
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/admin-panel
