Skip to main content

How to Add an Admin Dashboard to Your Boilerplate 2026

·StarterPick Team
admin-paneldashboardguideboilerplate2026

TL;DR

Building a usable admin panel takes 5-10 days from scratch. The main components: user list with search/filter, subscription management, basic metrics, and user impersonation. For boilerplates that include admin (Makerkit, Supastarter, Open SaaS), you skip this entirely. For ShipFast, T3 Stack, and others without admin, this guide covers the full implementation.


The Admin Panel Minimum Requirements

Before writing code, define what "admin" means for your SaaS:

Minimum Admin (every SaaS needs this):
- User list: view all users, search by email
- User detail: view account status, subscription, activity
- Subscription management: see plan, cancel, extend trial
- Basic metrics: total users, MRR, churn rate

Standard Admin (most B2B SaaS):
- User impersonation (sign in as any user)
- Subscription override (manually add access)
- Activity log (what users are doing)
- Support ticketing or Intercom integration

Advanced Admin (enterprise features):
- Custom permissions system
- Audit logs with export
- Bulk operations
- API usage monitoring

Step 1: Admin Route Protection

// app/admin/layout.tsx
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import { authOptions } from '~/lib/auth';

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await getServerSession(authOptions);

  if (!session) redirect('/login');

  // Check admin role — adjust to your user model
  if (session.user.role !== 'admin') {
    redirect('/dashboard?error=unauthorized');
  }

  return (
    <div className="admin-layout">
      <AdminSidebar />
      <main>{children}</main>
    </div>
  );
}

// prisma/schema.prisma — add role to User
model User {
  id    String    @id @default(cuid())
  email String    @unique
  role  UserRole  @default(USER)
  // ... other fields
}

enum UserRole {
  USER
  ADMIN
  SUPERADMIN
}

// Set your first admin manually
// prisma/seed.ts
await prisma.user.update({
  where: { email: 'your@email.com' },
  data: { role: 'ADMIN' }
});

Step 2: User Management Table

// app/admin/users/page.tsx
import { prisma } from '~/lib/prisma';

export default async function AdminUsersPage({
  searchParams,
}: {
  searchParams: { q?: string; page?: string };
}) {
  const query = searchParams.q ?? '';
  const page = parseInt(searchParams.page ?? '1');
  const limit = 50;

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      where: query ? {
        OR: [
          { email: { contains: query, mode: 'insensitive' } },
          { name: { contains: query, mode: 'insensitive' } },
        ],
      } : {},
      include: {
        subscription: true,
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
    }),
    prisma.user.count({
      where: query ? {
        OR: [
          { email: { contains: query, mode: 'insensitive' } },
          { name: { contains: query, mode: 'insensitive' } },
        ],
      } : {},
    }),
  ]);

  return (
    <div>
      <AdminSearch placeholder="Search by email or name..." />
      <UserTable users={users} />
      <Pagination total={total} page={page} limit={limit} />
    </div>
  );
}

Step 3: Metrics Dashboard

// app/admin/page.tsx — main metrics dashboard
async function getAdminMetrics() {
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);

  const [
    totalUsers,
    newUsersThisMonth,
    activeSubscriptions,
    mrr,
    churnedThisMonth,
  ] = await Promise.all([
    prisma.user.count(),
    prisma.user.count({ where: { createdAt: { gte: monthStart } } }),
    prisma.subscription.count({ where: { status: 'active' } }),
    prisma.subscription.aggregate({
      where: { status: 'active' },
      _sum: { priceMonthly: true }
    }),
    prisma.subscription.count({
      where: {
        status: 'canceled',
        updatedAt: { gte: monthStart }
      }
    }),
  ]);

  const mrrValue = (mrr._sum.priceMonthly ?? 0) / 100;
  const churnRate = activeSubscriptions > 0
    ? (churnedThisMonth / activeSubscriptions) * 100
    : 0;

  return {
    totalUsers,
    newUsersThisMonth,
    activeSubscriptions,
    mrr: mrrValue,
    churnRate: churnRate.toFixed(1),
  };
}

export default async function AdminDashboard() {
  const metrics = await getAdminMetrics();

  return (
    <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
      <MetricCard label="Total Users" value={metrics.totalUsers} />
      <MetricCard label="New This Month" value={metrics.newUsersThisMonth} />
      <MetricCard label="Active Subscribers" value={metrics.activeSubscriptions} />
      <MetricCard label="MRR" value={`$${metrics.mrr.toLocaleString()}`} />
    </div>
  );
}

Step 4: User Impersonation

User impersonation lets you sign in as any user to debug issues:

// app/api/admin/impersonate/route.ts
import { getServerSession } from 'next-auth';
import { encode } from 'next-auth/jwt';

export async function POST(req: Request) {
  const adminSession = await getServerSession(authOptions);

  if (adminSession?.user.role !== 'admin') {
    return new Response('Forbidden', { status: 403 });
  }

  const { userId } = await req.json();
  const targetUser = await prisma.user.findUnique({ where: { id: userId } });
  if (!targetUser) return new Response('User not found', { status: 404 });

  // Create a session token for the target user
  // Store original admin ID for "return to admin" functionality
  const token = await encode({
    token: {
      id: targetUser.id,
      email: targetUser.email!,
      name: targetUser.name,
      impersonatedBy: adminSession.user.id, // Track original admin
    },
    secret: process.env.NEXTAUTH_SECRET!,
  });

  return new Response(JSON.stringify({ token }), {
    headers: { 'Content-Type': 'application/json' }
  });
}

Using Existing Admin Libraries

Building admin from scratch takes 5-10 days. Alternatives:

Admin panel libraries:

# shadcn/ui Data Table (most common)
npx shadcn-ui@latest add table

# React Admin (free, more complete)
npm install react-admin

# Refine (open source admin framework)
npm install @refinedev/core @refinedev/nextjs-router

React Admin with Prisma:

// dataProvider that connects React Admin to your Prisma API
// You implement REST endpoints, React Admin handles the UI

For most boilerplates without admin, React Admin or Refine reduces admin development from 5-10 days to 2-4 days.


Time Budget

ComponentDuration
Route protection + layout0.5 day
User list + search1 day
User detail page1 day
Metrics dashboard1 day
Subscription management1 day
User impersonation1 day
Polish + testing1 day
Total~6.5 days

Or: choose Makerkit/Supastarter which includes admin out of the box.


Find boilerplates with built-in admin panels on StarterPick.

Check out this boilerplate

View Makerkit on StarterPick →

Comments