Admin Panel

Manage users, monitor subscriptions, analyze growth metrics, and maintain your SaaS with a powerful admin dashboard.

Admin Panel Overview

RankThis includes a complete admin panel for managing your growing SaaS business with real-time analytics, user management, and subscription oversight.

🎛️ Management Tools

  • • User account management
  • • Subscription monitoring
  • • Waitlist management
  • • Revenue analytics
  • • Growth metrics dashboard
  • • System health monitoring

📊 Analytics Features

  • • Monthly recurring revenue (MRR)
  • • User growth charts
  • • Subscription conversion rates
  • • Churn analysis
  • • Top performing features
  • • Geographic user distribution

Admin Access Control

Admin Role Setup

Secure admin access with role-based permissions:

Admin authentication setup
1// Update user schema to include admin role
2model User {
3 id String @id @default(cuid())
4 name String?
5 email String @unique
6 emailVerified DateTime?
7 image String?
8 role String @default("user") // Add role field
9
10 accounts Account[]
11 sessions Session[]
12 subscriptions Subscription[]
13
14 @@map("users")
15}
16
17// Admin role check utility
18export function isAdmin(user: User | null): boolean {
19 return user?.role === "admin";
20}
21
22// Protected admin route middleware
23import { auth } from "~/auth";
24import { redirect } from "next/navigation";
25
26export async function requireAdmin() {
27 const session = await auth();
28
29 if (!session?.user || !isAdmin(session.user)) {
30 redirect("/auth/signin");
31 }
32
33 return session.user;
34}

🔐 Security Note

Manually set the first admin user in your database or create a secure setup script. Never expose admin role assignment in your UI.

Admin Route Protection

Protect admin routes with authentication middleware:

src/app/admin/layout.tsx
1// src/app/admin/layout.tsx
2import { requireAdmin } from "~/lib/auth-utils";
3import { redirect } from "next/navigation";
4
5export default async function AdminLayout({
6 children,
7}: {
8 children: React.ReactNode;
9}) {
10 const user = await requireAdmin();
11
12 return (
13 <div className="min-h-screen bg-background">
14 <nav className="border-b">
15 <div className="flex h-16 items-center px-6">
16 <h1 className="text-xl font-semibold">Admin Dashboard</h1>
17 <div className="ml-auto flex items-center gap-4">
18 <span className="text-sm text-muted-foreground">
19 {user.email}
20 </span>
21 <Badge variant="secondary">Admin</Badge>
22 </div>
23 </div>
24 </nav>
25
26 <main className="p-6">
27 {children}
28 </main>
29 </div>
30 );
31}

Dashboard Components

Analytics Dashboard

Real-time business metrics and growth analytics:

src/components/admin/AnalyticsCharts.tsx
1"use client";
2
3import { useState, useEffect } from "react";
4import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
5import { useAllAnalytics } from "~/hooks/useAnalytics";
6
7export function AnalyticsCharts() {
8 const { growth, isLoading, isError } = useAllAnalytics();
9
10 if (isLoading) {
11 return <div>Loading analytics...</div>;
12 }
13
14 if (isError || !growth.data) {
15 return <div>Error loading analytics data.</div>;
16 }
17
18 const analytics = growth.data;
19
20 return (
21 <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
22 {/* Key Metrics */}
23 <Card>
24 <CardHeader>
25 <CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
26 </CardHeader>
27 <CardContent>
28 <div className="text-2xl font-bold">
29 ${analytics.totalMRR.toLocaleString()}
30 </div>
31 <p className="text-xs text-muted-foreground">
32 +{analytics.mrrGrowthRate}% from last month
33 </p>
34 </CardContent>
35 </Card>
36
37 <Card>
38 <CardHeader>
39 <CardTitle className="text-sm font-medium">Active Users</CardTitle>
40 </CardHeader>
41 <CardContent>
42 <div className="text-2xl font-bold">
43 {analytics.totalUsers.toLocaleString()}
44 </div>
45 <p className="text-xs text-muted-foreground">
46 +{analytics.customerGrowthRate}% from last month
47 </p>
48 </CardContent>
49 </Card>
50
51 <Card>
52 <CardHeader>
53 <CardTitle className="text-sm font-medium">Average Revenue</CardTitle>
54 </CardHeader>
55 <CardContent>
56 <div className="text-2xl font-bold">
57 ${analytics.averageRevenuePerUser.toFixed(2)}
58 </div>
59 <p className="text-xs text-muted-foreground">
60 Per user per month
61 </p>
62 </CardContent>
63 </Card>
64
65 <Card>
66 <CardHeader>
67 <CardTitle className="text-sm font-medium">Lifetime Value</CardTitle>
68 </CardHeader>
69 <CardContent>
70 <div className="text-2xl font-bold">
71 ${analytics.lifetimeValue.toFixed(2)}
72 </div>
73 <p className="text-xs text-muted-foreground">
74 Average customer LTV
75 </p>
76 </CardContent>
77 </Card>
78 </div>
79 );
80}
User Management

Comprehensive user management with search and filtering:

src/components/admin/UserManagement.tsx
1"use client";
2
3import { useState, useEffect } from "react";
4import { Button } from "~/components/ui/button";
5import { Badge } from "~/components/ui/badge";
6import { Input } from "~/components/ui/input";
7
8interface User {
9 id: string;
10 email: string;
11 name?: string;
12 role: string;
13 createdAt: Date;
14 subscription?: {
15 status: string;
16 plan: string;
17 };
18}
19
20export function UserManagement() {
21 const [users, setUsers] = useState<User[]>([]);
22 const [search, setSearch] = useState("");
23 const [filter, setFilter] = useState("all");
24
25 useEffect(() => {
26 fetchUsers();
27 }, []);
28
29 async function fetchUsers() {
30 const response = await fetch("/api/admin/users");
31 const data = await response.json();
32 setUsers(data.users);
33 }
34
35 const filteredUsers = users.filter(user => {
36 const matchesSearch = user.email.toLowerCase().includes(search.toLowerCase()) ||
37 user.name?.toLowerCase().includes(search.toLowerCase());
38
39 const matchesFilter = filter === "all" ||
40 (filter === "subscribed" && user.subscription) ||
41 (filter === "free" && !user.subscription);
42
43 return matchesSearch && matchesFilter;
44 });
45
46 return (
47 <div className="space-y-4">
48 {/* Filters */}
49 <div className="flex gap-4">
50 <Input
51 placeholder="Search users..."
52 value={search}
53 onChange={(e) => setSearch(e.target.value)}
54 className="max-w-sm"
55 />
56
57 <select
58 value={filter}
59 onChange={(e) => setFilter(e.target.value)}
60 className="rounded border px-3 py-2"
61 >
62 <option value="all">All Users</option>
63 <option value="subscribed">Subscribed</option>
64 <option value="free">Free Users</option>
65 </select>
66 </div>
67
68 {/* Users Table */}
69 <div className="overflow-x-auto">
70 <table className="w-full border-collapse border">
71 <thead>
72 <tr className="border-b bg-muted/50">
73 <th className="p-3 text-left">User</th>
74 <th className="p-3 text-left">Status</th>
75 <th className="p-3 text-left">Plan</th>
76 <th className="p-3 text-left">Joined</th>
77 <th className="p-3 text-left">Actions</th>
78 </tr>
79 </thead>
80 <tbody>
81 {filteredUsers.map((user) => (
82 <tr key={user.id} className="border-b">
83 <td className="p-3">
84 <div>
85 <div className="font-medium">{user.name || "Anonymous"}</div>
86 <div className="text-sm text-muted-foreground">{user.email}</div>
87 </div>
88 </td>
89 <td className="p-3">
90 <Badge variant={user.subscription ? "default" : "secondary"}>
91 {user.subscription?.status || "Free"}
92 </Badge>
93 </td>
94 <td className="p-3">
95 {user.subscription?.plan || "-"}
96 </td>
97 <td className="p-3 text-sm text-muted-foreground">
98 {new Date(user.createdAt).toLocaleDateString()}
99 </td>
100 <td className="p-3">
101 <Button variant="outline" size="sm">
102 View Details
103 </Button>
104 </td>
105 </tr>
106 ))}
107 </tbody>
108 </table>
109 </div>
110 </div>
111 );
112}

Admin Actions

Server Actions

Powerful server actions for admin operations:

src/server/actions/admin.ts
1"use server";
2
3import { db } from "~/server/db";
4import { requireAdmin } from "~/lib/auth-utils";
5import { revalidatePath } from "next/cache";
6
7export async function getAdminAnalytics() {
8 await requireAdmin();
9
10 const [
11 totalUsers,
12 activeSubscriptions,
13 totalRevenue,
14 waitlistCount,
15 ] = await Promise.all([
16 db.user.count(),
17 db.subscription.count({ where: { status: "active" } }),
18 db.subscription.aggregate({
19 where: { status: "active" },
20 _sum: { amount: true },
21 }),
22 db.waitlistEntry.count({ where: { isActive: true } }),
23 ]);
24
25 // Calculate monthly growth
26 const lastMonth = new Date();
27 lastMonth.setMonth(lastMonth.getMonth() - 1);
28
29 const newUsersThisMonth = await db.user.count({
30 where: { createdAt: { gte: lastMonth } },
31 });
32
33 return {
34 totalUsers,
35 activeSubscriptions,
36 totalRevenue: totalRevenue._sum.amount || 0,
37 waitlistCount,
38 newUsersThisMonth,
39 userGrowth: Math.round((newUsersThisMonth / Math.max(totalUsers - newUsersThisMonth, 1)) * 100),
40 };
41}
42
43export async function getUsersForAdmin(page: number = 1, limit: number = 50) {
44 await requireAdmin();
45
46 const users = await db.user.findMany({
47 take: limit,
48 skip: (page - 1) * limit,
49 orderBy: { createdAt: "desc" },
50 include: {
51 subscriptions: {
52 where: { status: "active" },
53 select: { status: true, plan: true },
54 },
55 },
56 });
57
58 const total = await db.user.count();
59
60 return {
61 users: users.map(user => ({
62 ...user,
63 subscription: user.subscriptions[0] || null,
64 })),
65 pagination: {
66 page,
67 limit,
68 total,
69 pages: Math.ceil(total / limit),
70 },
71 };
72}
73
74export async function updateUserRole(userId: string, role: string) {
75 await requireAdmin();
76
77 await db.user.update({
78 where: { id: userId },
79 data: { role },
80 });
81
82 revalidatePath("/admin");
83 return { success: true };
84}
85
86export async function deactivateUser(userId: string) {
87 await requireAdmin();
88
89 // Cancel active subscriptions
90 await db.subscription.updateMany({
91 where: {
92 userId,
93 status: "active",
94 },
95 data: { status: "canceled" },
96 });
97
98 // Optionally delete user data or mark as inactive
99 await db.user.update({
100 where: { id: userId },
101 data: { role: "inactive" },
102 });
103
104 revalidatePath("/admin");
105 return { success: true };
106}
Analytics Hook

Custom hook for real-time analytics data:

src/hooks/useAnalytics.ts
1"use client";
2
3import { useState, useEffect } from "react";
4
5interface AnalyticsData {
6 totalUsers: number;
7 activeSubscriptions: number;
8 totalRevenue: number;
9 waitlistCount: number;
10 userGrowth: number;
11 revenueGrowth: number;
12 churnRate: number;
13}
14
15export function useAnalytics() {
16 const [analytics, setAnalytics] = useState<AnalyticsData | null>(null);
17 const [isLoading, setIsLoading] = useState(true);
18 const [error, setError] = useState<string | null>(null);
19
20 useEffect(() => {
21 async function fetchAnalytics() {
22 try {
23 const response = await fetch("/api/admin/analytics");
24
25 if (!response.ok) {
26 throw new Error("Failed to fetch analytics");
27 }
28
29 const data = await response.json();
30 setAnalytics(data);
31 setError(null);
32 } catch (err) {
33 setError(err instanceof Error ? err.message : "Unknown error");
34 } finally {
35 setIsLoading(false);
36 }
37 }
38
39 fetchAnalytics();
40
41 // Refresh analytics every 5 minutes
42 const interval = setInterval(fetchAnalytics, 5 * 60 * 1000);
43
44 return () => clearInterval(interval);
45 }, []);
46
47 return { analytics, isLoading, error };
48}

Admin API Endpoints

RESTful Admin APIs

Secure API endpoints for admin operations:

Admin API examples
1// GET /api/admin/analytics - Get business analytics
2export async function GET() {
3 try {
4 const analytics = await getAdminAnalytics();
5 return Response.json(analytics);
6 } catch (error) {
7 return Response.json(
8 { error: "Unauthorized" },
9 { status: 401 }
10 );
11 }
12}
13
14// GET /api/admin/users - Get paginated users
15export async function GET(request: Request) {
16 const { searchParams } = new URL(request.url);
17 const page = parseInt(searchParams.get("page") || "1");
18 const limit = parseInt(searchParams.get("limit") || "50");
19
20 try {
21 const result = await getUsersForAdmin(page, limit);
22 return Response.json(result);
23 } catch (error) {
24 return Response.json(
25 { error: "Unauthorized" },
26 { status: 401 }
27 );
28 }
29}
30
31// POST /api/admin/users/[id]/role - Update user role
32export async function POST(
33 request: Request,
34 { params }: { params: { id: string } }
35) {
36 const { role } = await request.json();
37
38 try {
39 await updateUserRole(params.id, role);
40 return Response.json({ success: true });
41 } catch (error) {
42 return Response.json(
43 { error: "Unauthorized" },
44 { status: 401 }
45 );
46 }
47}
48
49// DELETE /api/admin/users/[id] - Deactivate user
50export async function DELETE(
51 request: Request,
52 { params }: { params: { id: string } }
53) {
54 try {
55 await deactivateUser(params.id);
56 return Response.json({ success: true });
57 } catch (error) {
58 return Response.json(
59 { error: "Unauthorized" },
60 { status: 401 }
61 );
62 }
63}
🔒 Security Best Practices

Authentication

  • • Always verify admin role server-side
  • • Use session-based authentication
  • • Implement rate limiting on admin APIs
  • • Log all admin actions for audit

Data Protection

  • • Never expose sensitive user data
  • • Hash/encrypt sensitive information
  • • Implement proper data retention
  • • Follow GDPR/privacy regulations
🚨 Monitoring & Alerts

Key Metrics to Monitor

  • • Failed payment rates
  • • User churn patterns
  • • System performance metrics
  • • Error rates and exceptions

Alert Setup Example

Alert system
1// Set up alerts for critical metrics
2if (analytics.churnRate > 10) {
3 await sendSlackAlert('High churn rate detected: ' + analytics.churnRate + '%');
4}
5
6if (analytics.revenueGrowth < -5) {
7 await sendEmailAlert('Revenue declining', adminEmails);
8}

Admin Panel Complete!

Your admin dashboard is ready for managing users, monitoring growth, and scaling your SaaS. Next, set up the blog system for content marketing.