SaaS admin dashboard
An admin dashboard is an internal team tool: managing tenants, users, billing, and viewing metrics. Not to be confused with the customer dashboard.
Architecture: separate route with strict authorization
// middleware.ts
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
if (pathname.startsWith('/admin')) {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'SUPER_ADMIN') {
return NextResponse.redirect(new URL('/login', request.url));
}
// Additionally: IP whitelist for admin
const clientIp = request.headers.get('x-forwarded-for');
const allowedIps = process.env.ADMIN_ALLOWED_IPS?.split(',') ?? [];
if (allowedIps.length > 0 && !allowedIps.includes(clientIp ?? '')) {
return new NextResponse('Forbidden', { status: 403 });
}
}
}
Business metrics
// app/admin/page.tsx
export default async function AdminDashboard() {
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const [
totalTenants,
activeTenants,
newTenants30d,
mrr,
churnedTenants30d,
totalUsers,
recentErrors,
] = await Promise.all([
db.tenant.count(),
db.tenant.count({ where: { status: 'ACTIVE' } }),
db.tenant.count({
where: { createdAt: { gte: thirtyDaysAgo }, status: 'ACTIVE' }
}),
calculateMRR(),
db.subscription.count({
where: { status: 'CANCELED', canceledAt: { gte: thirtyDaysAgo } }
}),
db.user.count(),
getRecentErrors(),
]);
const churnRate = activeTenants > 0
? ((churnedTenants30d / activeTenants) * 100).toFixed(1)
: '0';
return (
<AdminLayout>
<MetricGrid>
<MetricCard title="Active tenants" value={activeTenants} />
<MetricCard title="New (30 days)" value={newTenants30d} />
<MetricCard title="MRR" value={`$${(mrr / 100).toFixed(0)}`} />
<MetricCard title="Churn rate" value={`${churnRate}%`} variant={parseFloat(churnRate) > 5 ? 'danger' : 'normal'} />
</MetricGrid>
<RecentErrorsWidget errors={recentErrors} />
</AdminLayout>
);
}
async function calculateMRR(): Promise<number> {
const subscriptions = await db.subscription.findMany({
where: { status: 'ACTIVE' },
select: { stripePriceId: true }
});
let mrr = 0;
for (const sub of subscriptions) {
const price = await stripe.prices.retrieve(sub.stripePriceId!);
const monthly = price.recurring?.interval === 'year'
? price.unit_amount! / 12
: price.unit_amount!;
mrr += monthly;
}
return mrr;
}
Tenant management
// app/admin/tenants/page.tsx
export default async function TenantsAdminPage({
searchParams
}: {
searchParams: { q?: string; plan?: string; status?: string; page?: string }
}) {
const page = parseInt(searchParams.page ?? '1');
const pageSize = 25;
const tenants = await db.tenant.findMany({
where: {
...(searchParams.q ? {
OR: [
{ slug: { contains: searchParams.q, mode: 'insensitive' } },
{ name: { contains: searchParams.q, mode: 'insensitive' } },
]
} : {}),
...(searchParams.plan ? { plan: searchParams.plan as Plan } : {}),
...(searchParams.status ? { status: searchParams.status as TenantStatus } : {}),
},
include: {
subscription: true,
_count: { select: { users: true } }
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
});
return (
<div>
<TenantFilters />
<TenantTable tenants={tenants} />
<Pagination page={page} pageSize={pageSize} />
</div>
);
}
Impersonation: logging in as a tenant
// admin can log in as any user for diagnostics
export async function impersonateTenant(tenantId: string) {
'use server';
const adminSession = await auth();
if (adminSession?.user.role !== 'SUPER_ADMIN') {
throw new Error('Unauthorized');
}
// Log the action
await db.adminAuditLog.create({
data: {
adminId: adminSession.user.id,
action: 'IMPERSONATE_TENANT',
targetId: tenantId,
metadata: { reason: 'admin_requested' },
}
});
const tenant = await db.tenant.findUniqueOrThrow({
where: { id: tenantId },
include: { users: { take: 1, orderBy: { role: 'asc' } } }
});
// Set impersonation cookie
const cookieStore = cookies();
cookieStore.set('impersonate_tenant_id', tenantId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60, // 1 hour
});
redirect(`https://${tenant.slug}.${process.env.ROOT_DOMAIN}/dashboard`);
}
Audit log
model AdminAuditLog {
id String @id @default(cuid())
adminId String
action String // 'IMPERSONATE_TENANT' | 'CANCEL_SUBSCRIPTION' | 'REFUND' | ...
targetId String? // ID of tenant, user, or invoice
metadata Json?
ip String?
createdAt DateTime @default(now())
admin User @relation(fields: [adminId], references: [id])
}
// All admin actions are logged
export async function cancelTenantSubscription(tenantId: string, reason: string) {
const session = await auth();
await db.adminAuditLog.create({
data: {
adminId: session!.user.id,
action: 'CANCEL_SUBSCRIPTION',
targetId: tenantId,
metadata: { reason },
}
});
const subscription = await db.subscription.findUnique({ where: { tenantId } });
await stripe.subscriptions.cancel(subscription!.stripeSubscriptionId!);
}
Developing an admin dashboard with metrics, tenant management, and audit logs — 5–8 working days.







