Spacerr includes a protected admin dashboard for managing users, roles, account access, and support workflows. The admin area is not part of the public product surface. It is guarded by role checks and private route metadata.
What It Includes
The admin surface includes:
- User list with avatar, name, email, role, chat count, purchase state, created date, last active date, and deactivation state.
- Total user, active user, subscribed user, and deactivated account stats.
- User email and role editing.
- User deletion with self deletion blocked.
- User impersonation through the Better Auth admin client.
- Magic link sending from the admin panel.
- Admin, moderator, and user roles.
Route And Access
The admin page lives at src/app/(app)/dashboard/admin/page.tsx and is available at:
/dashboard/adminThe page reads the current session and only renders for admin users.
const user = await getSessionUser()
if (!user || !isAdmin(user)) {
redirect(WebRoutes.dashboard.path)
}
return <AdminUsersTable currentUserId={user.id} />Admin API routes perform the same role check before returning data or mutating users.
const user = await getSessionUser()
if (!user || !isAdmin(user)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}User Roles
Use admin for full admin dashboard access. Use moderator for protected content workflows such as blog publishing. Use user for normal product access.
To create the first admin, create a normal account first, then update the user's role to admin in the database. The Database section covers the recommended database editing options.
API Layer
Admin API paths are centralized in ApiRoutes.
export const ApiRoutes = {
admin: {
users: {
list: "/api/admin/users",
sendMagicLinks: "/api/admin/users/magic-links",
update: (userId: string) => `/api/admin/users/${userId}`,
delete: (userId: string) => `/api/admin/users/${userId}`,
},
},
}Browser requests go through src/features/admin/api/admin-users.api.ts, and data loading goes through TanStack Query hooks under src/features/admin/hooks.
User Management
The user table data comes from src/features/admin/repositories/admin-users.repository.ts. Keep admin data shaping in the repository, then return a UI friendly response to the table.
async function getAdminUserSubscriptionStatus(user: {
id: string
oneTimePurchasedAt: Date | null
stripeCustomerId: string | null
}) {
const activeSubscriptionProduct = user.stripeCustomerId
? await getActiveSubscriptionProduct(user.stripeCustomerId, user.id)
: null
if (user.oneTimePurchasedAt || activeSubscriptionProduct) {
return "active"
}
return "inactive"
}
export async function listAdminUsers(): Promise<AdminUsersListResponse> {
const { totalUsers, users } = await loadAdminUsers()
const adminUsers = await Promise.all(
users.map(async (user) => ({
id: user.id,
name: user.name,
image: user.image,
email: user.email,
role: user.role,
chatsCount: user._count.chats,
createdAt: user.createdAt.toISOString(),
lastActiveAt: user.lastActiveAt?.toISOString() ?? null,
deactivatedAt: user.deactivatedAt?.toISOString() ?? null,
subscriptionStatus: await getAdminUserSubscriptionStatus(user),
}))
)
return {
totalUsers,
users: adminUsers,
}
}Keep admin data shaping in the repository. The starter stores one time access on the user record with oneTimePurchasedAt, keeps the Stripe customer ID as stripeCustomerId, and resolves active subscription access from Stripe. The table can then show one clear access status while still letting you change the billing model behind it. Keep route handlers thin and limited to auth checks, validation, and response handling.
Magic Links And Impersonation
Admins can send magic links for account access support and impersonate a user when they need to reproduce account specific issues.
export function useMutateImpersonateAdminUser() {
return useMutation({
mutationFn: async (userId: string) => authClient.admin.impersonateUser({ userId }),
})
}Use impersonation carefully. It is intended for support and debugging, not normal product usage.
SEO And Privacy
Admin routes should stay private. The admin page uses noindex metadata, and admin API responses include noindex headers.
const noIndexHeaders = {
"X-Robots-Tag": "noindex, nofollow, noarchive, nosnippet",
}Do not add admin pages to public navigation, sitemap priority, marketing copy, or structured data.
Where To Customize
Use these files first:
src/features/admin/components/admin-users-table.tsxfor the dashboard table.src/features/admin/components/admin-users-columns.tsxfor visible columns and row actions.src/features/admin/components/admin-user-edit-dialog.tsxfor editing email and role.src/features/admin/components/admin-send-magic-links-card.tsxfor magic link sending.src/features/admin/repositories/admin-users.repository.tsfor admin data shaping.src/features/admin/schemasfor admin request validation.src/app/api/admin/usersfor admin route handlers.