Overview
The admin panel at /admin provides system administrators with tools to manage the entire platform. Access is restricted to users with isSystemAdmin: true.
Authentication & Authorization
System Admin Flag
Defined in user schema at lib/db/schema.ts:68:
export const user = pgTable("user", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
// System admin flag (for super admin access across all workspaces)
isSystemAdmin: boolean("is_system_admin").notNull().default(false),
// User can also be banned
banned: boolean("banned").notNull().default(false),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
});
Access Control
Implemented in lib/admin-auth.ts:
export async function requireSystemAdmin() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
redirect("/sign-in?callbackUrl=/admin");
}
const [currentUser] = await db
.select({ isSystemAdmin: user.isSystemAdmin })
.from(user)
.where(eq(user.id, session.user.id))
.limit(1);
if (!currentUser?.isSystemAdmin) {
redirect("/dashboard"); // Non-admins get redirected
}
return { session, userId: currentUser.id };
}
Only users with isSystemAdmin: true can access /admin routes. This flag must be set directly in the database.
Setting Initial Admin
Manually promote a user to system admin:
Admin Layout
Defined in app/admin/layout.tsx:13:
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
// Verify system admin access - redirects if not authorized
await requireSystemAdmin();
return (
<div className="min-h-screen bg-background">
<ImpersonationBanner />
<AdminHeader />
<main className="w-full py-6">{children}</main>
<Toaster />
</div>
);
}
Every admin page automatically:
- Checks system admin status
- Shows impersonation banner (if active)
- Renders admin navigation header
Admin Sections
Dashboard (/admin)
Overview with key metrics:
- Total workspaces
- Active users
- Monthly revenue
- Recent activity
Workspaces (/admin/workspaces)
Manage all customer workspaces:
List View:
- Search and filter workspaces
- View plan and status
- Quick actions (suspend, edit)
Detail View (/admin/workspaces/[id]):
Implemented in app/admin/workspaces/[id]/page.tsx:
export default async function AdminWorkspaceDetailPage({ params }: PageProps) {
await requireSystemAdmin();
const { id } = await params;
const data = await getAdminWorkspaceDetail(id);
return <WorkspaceDetailContent workspace={data} />;
}
Features:
- Edit workspace details
- Change plan (free/pro/enterprise)
- Update status (active/suspended/trial)
- Toggle invoice eligibility
- View usage statistics
- Manage workspace members
- Impersonate workspace owner
Users (/admin/users)
Manage all platform users:
List View:
- Search by name or email
- Filter by role or workspace
- View last login time
Detail View (/admin/users/[id]):
- User profile and activity
- Ban/unban users
- Reset passwords
- Change user roles
- View user’s projects
User Roles:
From lib/db/schema.ts:428:
export type UserRole = "owner" | "admin" | "member";
- Owner - Full workspace control, billing access
- Admin - Can manage projects and users
- Member - Can create and edit own projects
Billing (/admin/billing)
Manage invoices and payments.
Stats Bar:
From app/admin/billing/page.tsx:22:
const formattedStats = {
uninvoicedCount: stats.uninvoicedCount,
uninvoicedAmount: stats.uninvoicedAmountOre / 100,
pendingPayment: stats.pendingPaymentCount,
pendingPaymentAmount: stats.pendingPaymentAmountOre / 100,
invoicedThisMonth: stats.invoicedThisMonthCount,
invoicedAmountThisMonth: stats.invoicedThisMonthAmountOre / 100,
};
Uninvoiced Tab:
- Projects awaiting invoice generation
- Group by workspace
- Batch invoice creation
- Export to Fiken accounting
Invoice History Tab:
- All generated invoices
- Filter by status (draft/sent/paid/overdue)
- View line items
- Download PDFs
- Sync with Fiken
Revenue (/admin/revenue)
Analytics and reporting:
- Revenue trends over time
- Revenue by plan type
- Top customers by spend
- Payment method distribution
Affiliates (/admin/affiliates)
Manage affiliate relationships:
- Link referring workspace to referred workspace
- Set commission percentage
- Track affiliate earnings
- Process payouts
Affiliate Schema:
From lib/db/schema.ts:605:
export const affiliateRelationship = pgTable("affiliate_relationship", {
id: text("id").primaryKey(),
affiliateWorkspaceId: text("affiliate_workspace_id").notNull(),
referredWorkspaceId: text("referred_workspace_id").notNull(),
commissionPercent: integer("commission_percent").notNull().default(20),
isActive: boolean("is_active").notNull().default(true),
});
export const affiliateEarning = pgTable("affiliate_earning", {
id: text("id").primaryKey(),
affiliateWorkspaceId: text("affiliate_workspace_id").notNull(),
invoiceId: text("invoice_id").notNull(),
invoiceAmountOre: integer("invoice_amount_ore").notNull(),
commissionPercent: integer("commission_percent").notNull(),
earningAmountOre: integer("earning_amount_ore").notNull(),
status: text("status").notNull().default("pending"), // 'pending' | 'paid_out'
});
Admin Actions
Workspace Management
Update Workspace Plan
await db.update(workspace)
.set({
plan: "enterprise",
updatedAt: new Date(),
})
.where(eq(workspace.id, workspaceId));
Suspend Workspace
await db.update(workspace)
.set({
status: "suspended",
suspendedAt: new Date(),
suspendedReason: "Payment overdue",
})
.where(eq(workspace.id, workspaceId));
Enable Invoice Billing
From lib/actions/payments.ts:393:
export async function setWorkspaceInvoiceEligibility(
workspaceId: string,
eligible: boolean
): Promise<ActionResult<{ success: boolean }>> {
const adminCheck = await verifySystemAdmin();
if (adminCheck.error) {
return { success: false, error: adminCheck.error };
}
await db.update(workspace)
.set({
invoiceEligible: eligible,
invoiceEligibleAt: eligible ? new Date() : null,
})
.where(eq(workspace.id, workspaceId));
}
User Management
Ban User
await db.update(user)
.set({
banned: true,
banReason: "Terms of service violation",
banExpires: null, // Permanent ban
})
.where(eq(user.id, userId));
Temporary Ban
await db.update(user)
.set({
banned: true,
banReason: "Suspicious activity",
banExpires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
})
.where(eq(user.id, userId));
User Impersonation
How It Works
Better-auth admin plugin provides impersonation tracking:
export const session = pgTable("session", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
// Admin impersonation tracking (better-auth admin plugin)
impersonatedBy: text("impersonated_by").references(() => user.id),
});
Impersonation Banner
From app/admin/layout.tsx:23:
Shows prominent warning when admin is impersonating another user.
Impersonation gives full access to the user’s account. Use only for debugging and support. All actions are logged with the admin’s ID.
Workspace Invitation System
Admins can invite workspaces for special programs:
export const workspace = pgTable("workspace", {
// Admin can manually invite workspaces (for beta, referrals, etc.)
invitedByAdmin: boolean("invited_by_admin").notNull().default(false),
});
Invited workspaces may receive:
- Extended trial period
- Custom pricing
- Priority support
- Early access to features
Querying Admin Data
Get Workspace with Details
Implemented in lib/db/queries.ts:
export async function getAdminWorkspaceDetail(workspaceId: string) {
const workspace = await db.query.workspace.findFirst({
where: eq(schema.workspace.id, workspaceId),
with: {
members: true,
projects: {
limit: 10,
orderBy: (projects, { desc }) => [desc(projects.createdAt)],
},
},
});
return workspace;
}
Get Billing Stats
export async function getBillingStats() {
const [uninvoiced, pending, thisMonth, total] = await Promise.all([
// Uninvoiced line items
db.select({ count: count() })
.from(invoiceLineItem)
.where(eq(invoiceLineItem.status, "pending")),
// Pending Stripe payments
db.select({ count: count() })
.from(projectPayment)
.where(eq(projectPayment.status, "pending")),
// Invoices this month
db.select({ count: count() })
.from(invoice)
.where(sql`EXTRACT(MONTH FROM issue_date) = EXTRACT(MONTH FROM NOW())`),
// Total invoiced
db.select({ sum: sum(invoice.totalAmountOre) })
.from(invoice),
]);
}
Admin Navigation
Implemented in components/admin/admin-header.tsx:
const navItems = [
{ href: "/admin", label: "Dashboard", icon: IconHome },
{ href: "/admin/workspaces", label: "Workspaces", icon: IconBuilding },
{ href: "/admin/users", label: "Users", icon: IconUsers },
{ href: "/admin/billing", label: "Billing", icon: IconFileInvoice },
{ href: "/admin/revenue", label: "Revenue", icon: IconChartBar },
{ href: "/admin/affiliates", label: "Affiliates", icon: IconUsersGroup },
];