Admin Panel

Manage users, monitor subscriptions, and analyze growth metrics.

Management

  • • User account management
  • • Subscription monitoring
  • • Waitlist management
  • • Revenue analytics

Analytics

  • • Monthly recurring revenue
  • • User growth charts
  • • Conversion rates
  • • Churn analysis

Adding Admins

Admin access is controlled via the ADMIN_EMAILS environment variable. Add a comma-separated list of admin emails:

.env
# Single admin
ADMIN_EMAILS="you@example.com"
# Multiple admins
ADMIN_EMAILS="you@example.com,cofounder@example.com,dev@example.com"

Local Development

Add to your .env file in the project root.

Production (Vercel)

Add via Settings → Environment Variables in your Vercel dashboard. Changes take effect on next deployment.

Benefits: No code changes needed to add/remove admins. Different admin lists per environment (dev vs production).

How It Works

The checkIsAdmin() function reads from the environment variable and compares against the logged-in user's email:

src/server/actions/admin.ts
1// src/server/actions/admin.ts
2export async function checkIsAdmin(): Promise<boolean> {
3 const session = await auth();
4 if (!session?.user?.email) return false;
5
6 // Read from ADMIN_EMAILS environment variable
7 const adminEmailsEnv = process.env.ADMIN_EMAILS ?? "";
8 const adminEmails = adminEmailsEnv
9 .split(",")
10 .map((email) => email.trim().toLowerCase())
11 .filter((email) => email.length > 0);
12
13 return adminEmails.includes(session.user.email.toLowerCase());
14}

Route Protection

Admin routes are protected server-side. The check happens on every request:

1// src/app/admin/page.tsx
2import { auth } from "~/auth";
3import { checkIsAdmin } from "~/server/actions/admin";
4import { redirect } from "next/navigation";
5
6export default async function AdminPage() {
7 const session = await auth();
8 if (!session) redirect("/auth/signin");
9
10 const isAdmin = await checkIsAdmin();
11 if (!isAdmin) return <div>Access Denied</div>;
12
13 return <AdminDashboard />;
14}

Admin status is determined by environment variables only. Never expose admin assignment in the UI.

Key Metrics

src/server/actions/admin.ts
1"use server";
2
3export async function getAdminAnalytics() {
4 await requireAdmin();
5
6 const [totalUsers, activeSubscriptions, waitlistCount] = await Promise.all([
7 db.user.count(),
8 db.subscription.count({ where: { status: "active" } }),
9 db.waitlistEntry.count({ where: { isActive: true } }),
10 ]);
11
12 const lastMonth = new Date();
13 lastMonth.setMonth(lastMonth.getMonth() - 1);
14
15 const newUsersThisMonth = await db.user.count({
16 where: { createdAt: { gte: lastMonth } },
17 });
18
19 return {
20 totalUsers,
21 activeSubscriptions,
22 waitlistCount,
23 newUsersThisMonth,
24 userGrowth: Math.round(
25 (newUsersThisMonth / Math.max(totalUsers - newUsersThisMonth, 1)) * 100
26 ),
27 };
28}

User Management

1"use server";
2
3export async function getUsersForAdmin(page = 1, limit = 50) {
4 await requireAdmin();
5
6 const [users, total] = await Promise.all([
7 db.user.findMany({
8 include: { subscription: { select: { type: true, status: true } } },
9 orderBy: { createdAt: "desc" },
10 skip: (page - 1) * limit,
11 take: limit,
12 }),
13 db.user.count(),
14 ]);
15
16 return {
17 users,
18 pagination: { page, limit, total, pages: Math.ceil(total / limit) },
19 };
20}

Security

Authentication

  • • Verify admin role server-side
  • • Use session-based auth
  • • Implement rate limiting
  • • Log all admin actions

Data Protection

  • • Never expose sensitive data
  • • Encrypt sensitive info
  • • Proper data retention
  • • Follow GDPR/privacy laws

Next: Account Management

User profile and billing pages

Continue →