How to Add an Admin Dashboard to Your Boilerplate 2026
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
| Component | Duration |
|---|---|
| Route protection + layout | 0.5 day |
| User list + search | 1 day |
| User detail page | 1 day |
| Metrics dashboard | 1 day |
| Subscription management | 1 day |
| User impersonation | 1 day |
| Polish + testing | 1 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 →