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.
Why Admin Matters for SaaS Operations
The admin panel is not a customer-facing feature, but it is the feature you'll use every day once you have customers. Without admin, every customer support query requires either SSH into the database or a raw SQL query. "Why isn't my account active?" "Can you reset my subscription?" "Did my webhook fire?" — all of these require admin access to diagnose and resolve in under 2 minutes.
The business cost of not having admin is disproportionate. Without it, you spend 30-60 minutes on support queries that should take 2 minutes. You can't spot patterns in failed subscriptions without a metrics dashboard. You can't debug a customer's workflow without user impersonation. The 5-10 days it takes to build admin pays back within the first month of having paying customers.
The build-or-buy calculus:
- Makerkit, Supastarter, Open SaaS: Admin included out of the box — choose these if you're starting fresh and admin is a priority
- React Admin, Refine: Open-source admin frameworks that reduce custom admin from 10 days to 2-4 days — use these if you're on an existing boilerplate
- Build from scratch: Only justified if your admin needs are highly custom (custom analytics, unusual workflows)
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' }
});
}
Impersonation requires a visible "impersonating as [user]" banner when active, and a one-click "return to admin" button. Without this, it's easy to accidentally perform actions as the wrong user. Store the original admin ID in the session token (shown above with impersonatedBy) and render the banner based on that field being present.
Activity Monitoring
Basic activity logging makes support much faster: you can see what a user did in the 30 minutes before they reported a problem.
// lib/activity.ts — structured activity log
export async function logActivity(
userId: string,
action: string,
metadata?: Record<string, unknown>
) {
await prisma.activityLog.create({
data: {
userId,
action,
metadata: metadata ? JSON.stringify(metadata) : null,
ipAddress: headers().get('x-forwarded-for')?.split(',')[0],
userAgent: headers().get('user-agent'),
},
});
}
// Call from API routes:
await logActivity(session.user.id, 'project.created', { projectId: project.id });
await logActivity(session.user.id, 'subscription.upgraded', { from: 'free', to: 'pro' });
In the admin user detail page, show the last 20 activity events for any user. This lets you reconstruct what a user was doing when they hit a bug, without asking them to describe it.
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 gives you sortable, filterable, searchable data tables out of the box. You implement REST endpoints that match its expected format, and React Admin handles the UI layer. It cuts admin development from 5-10 days to 2-3 days.
Refine is more flexible than React Admin and integrates natively with Next.js App Router. It supports custom UI (Ant Design, shadcn/ui, or headless) and has first-class Prisma and Supabase data providers. Better choice if you want admin that matches your existing design system.
For most SaaS products, the pragmatic choice is: shadcn/ui Data Table for the user list (already in your component library), custom server components for the metrics dashboard, and manual API routes for actions. Avoid React Admin unless your admin needs are complex enough to warrant learning its data provider model.
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 |
| Activity log | 0.5 day |
| Polish + testing | 0.5 day |
| Total | ~6.5 days |
Or: choose Makerkit/Supastarter which includes admin out of the box.
Subscription Management in Admin
The most common admin task after launch is subscription management: extending trials, granting manual access, handling payment failures, and processing refunds. Having these controls in admin eliminates the need for database-level access for routine customer support.
Trial extension: Add a server action that updates trialEndsAt on the user record. In admin, show a "Extend trial" button on the user detail page with a date picker. Common use case: a qualified lead asks for more time to evaluate before committing to a purchase.
Manual access grant: A boolean hasManualAccess field with an expiry date lets you grant free access to specific users without creating a Stripe subscription. Useful for investors, press, influencers, or extended trials for high-value prospects.
Subscription cancellation: Most SaaS founders avoid easy cancellation flows, but admin-level cancellation matters for handling refund requests quickly. A "Cancel subscription" button that calls stripe.subscriptions.cancel() and updates your local record handles this in one click. Chargeback risk goes down when refunds happen before the dispute window.
Payment failure handling (dunning): Stripe's Smart Retries handle the retry logic. Your admin panel should show which users have failed payments and for how long. Users who have been in past_due status for more than 7 days often need a manual email — Stripe's automatic emails sometimes go to spam.
Audit Logging for Compliance
Admin panels that allow data modification need audit logs: a record of who changed what and when. This matters for compliance (SOC2, GDPR) and for debugging when something unexpected changes in your production database.
// lib/audit.ts — simple audit log
export async function auditLog(
adminUserId: string,
action: string,
targetId: string,
targetType: string,
before?: unknown,
after?: unknown,
) {
await prisma.auditLog.create({
data: {
adminUserId,
action, // 'user.subscription_extended', 'user.impersonation_started'
targetId,
targetType, // 'user', 'organization', 'subscription'
before: before ? JSON.stringify(before) : null,
after: after ? JSON.stringify(after) : null,
},
});
}
Log every write action in admin: subscription modifications, trial extensions, manual access grants, user impersonation start/end, data deletions. The audit log is searchable in the admin panel and filterable by admin user, action type, and target.
For GDPR compliance, the audit log itself must be limited in retention (typically 90-365 days). Add a cleanup cron job that deletes audit records older than your retention policy.
Related Resources
For boilerplates with admin pre-built — including role management, user impersonation, and subscription controls — see best boilerplates for internal tools, which covers Refine and TanStack Table patterns. For adding RBAC beyond basic admin role checks, team and org management guide covers per-organization role hierarchies. For the subscription management side of admin, usage-based billing with Stripe covers the metered billing tables you'll want to expose in admin.
Search and Filtering in Admin
The user list becomes difficult to use past 500 users without proper filtering. Beyond text search, add filter controls that let admins segment by:
- Plan: Show all free users, or all pro users
- Status: Active, trialing, past_due, canceled subscriptions
- Signup date: Users who joined in a specific date range
- Last active: Users who haven't logged in for 30+ days (at-risk churn)
Each filter adds a where clause to your Prisma query. The URL should encode the active filters so admins can bookmark filtered views and share links to specific segments with their team.
For the most common admin operations — finding a user by email, checking their subscription status, extending a trial — keyboard-driven search with instant results is faster than any filter UI. A global search bar that queries name and email fields and returns top matches in under 200ms handles 80% of admin lookups.
Admin Analytics with Recharts
The metrics dashboard from Step 3 shows current totals, but trend charts are what you actually need for operational decisions. A line chart showing new signups per day over the last 30 days reveals whether your last marketing effort worked. A bar chart showing MRR by month shows growth trajectory.
Recharts is the standard charting library for Next.js apps and works well with shadcn/ui's color system. Add responsive line and bar charts to your admin dashboard with data from Prisma aggregations:
// Get daily signups for the last 30 days
const signupsByDay = await prisma.$queryRaw`
SELECT
DATE_TRUNC('day', "createdAt") as date,
COUNT(*)::int as count
FROM "User"
WHERE "createdAt" > NOW() - INTERVAL '30 days'
GROUP BY DATE_TRUNC('day', "createdAt")
ORDER BY date ASC
`;
Render with <LineChart> from Recharts. Use <ResponsiveContainer width="100%" height={200}> to make charts responsive without fixed pixel heights.
User Segmentation and Cohort Views
Beyond individual user lookup, admin panels that show user segments are significantly more useful for making product decisions:
- Plan breakdown: How many users are on Free vs Pro vs Enterprise?
- Cohort retention: Of the users who signed up 3 months ago, what percentage are still active?
- Feature adoption: What percentage of active users have used feature X?
- Geographic distribution: Where are your users located?
These views require additional queries against your user and event tables, but they answer the questions that drive product decisions: "Should we add more free tier features?" "Which cohort has the best retention?" "Is the new feature getting adopted?"
Start with plan breakdown and churn cohort (users created in the same month, tracked monthly) — these two charts answer most early-stage business questions.
Methodology
Implementation patterns based on NextAuth documentation and community patterns in the T3 Stack Discord. Time estimates based on survey data from ShipFast Discord members who added admin panels to their products.
Find boilerplates with built-in admin panels on StarterPick.
Check out this boilerplate
View Makerkiton StarterPick →