Adding Admin feature
Better Auth, which we already use for authentication in this boilerplate, offers a complete plugin system to extend its features.
The library provides a specific plugin for managing admin users with features like role-based access control, user impersonation, and ban management. The official Better Auth documentation covers this plugin and all its options in depth.
This guide will help you quickly configure the Admin plugin in the boilerplate with NestJS and MikroORM.
File Structure
Section titled “File Structure”Since the Admin plugin is part of Better Auth and works as an integrated system, we will add all admin-related fields to existing files in the /auth folder.
Here is the file structure we will modify:
apps/api/src/├── auth/│ ├── auth.entity.ts (to update)│ ├── auth.guard.ts (to update)│ └── auth.decorator.ts (to update)└── better-auth.config.ts (to update)Installation
Section titled “Installation”-
Add the plugin to Better Auth configuration
Section titled “Add the plugin to Better Auth configuration”In your Better Auth configuration file
better-auth.config.tson the API side, add theadminplugin:import { admin } from "better-auth/plugins"export const auth = betterAuth({// ... your existing configplugins: [admin({defaultRole: 'user',adminRoles: ['admin'],})]})
-
Update the User entity
Section titled “Update the User entity”The Admin plugin adds several fields to the user table. Update your
Userentity inauth.entity.ts:src/auth/auth.entity.ts @Entity({ tableName: 'auth_user' })export class User {// ... your existing properties (id, name, email, etc.)@Property()role!: string@Property({ type: 'boolean', default: false })banned!: boolean@Property({ fieldName: 'banReason', nullable: true })banReason: string | null = null@Property({ fieldName: 'banExpires', nullable: true })banExpires: Date | null = null// ... your existing timestamps and relations}Update the Session entity for impersonation
Section titled “Update the Session entity for impersonation”The Admin plugin also adds an
impersonatedByfield to track when an admin is impersonating another user:src/auth/auth.entity.ts @Entity({ tableName: 'auth_session' })export class Session {// ... your existing properties@Property({ nullable: true })impersonatedBy!: string | null// ... your existing properties}
-
Update the database
Section titled “Update the database”Now that you have updated the entities, you need to update your database to add the new columns.
Depending on your project stage, you can either recreate your database from scratch if you’re in development without important data, or use the migration system to preserve existing data.
-
Use admin features
Section titled “Use admin features”Now that everything is configured on the API side, you can use admin-related methods from your services.
Usage examples:
import { Injectable } from '@nestjs/common'import { EntityManager } from '@mikro-orm/postgresql'import { User } from './auth.entity'@Injectable()export class UsersService {constructor(private readonly em: EntityManager) {}async ban(id: string, data: BanUserDto): Promise<void> {const user = await this.em.findOne(User, { id })if (!user) {throw new NotFoundException('User not found')}this.em.assign(user, {banned: true,banReason: data.banReason,banExpires: data.banExpires,})await this.em.flush()}async unban(id: string): Promise<void> {const user = await this.em.findOne(User, { id })if (!user) {throw new NotFoundException('User not found')}this.em.assign(user, {banned: false,banReason: null,banExpires: null,})await this.em.flush()}}
The Admin plugin is now configured and operational. You can manage user roles, ban/unban users, and impersonate users via the Better Auth client.
Going further
Section titled “Going further”NestJS Decorators & Guards
Section titled “NestJS Decorators & Guards”The boilerplate already provides authentication decorators and guards. You can extend them to handle admin-specific logic.
Add admin-related decorators:
import { SetMetadata } from '@nestjs/common'
// ... your existing decorators (@Session, @Public, etc.)
/** * Decorator to mark routes that require super admin access */export const SuperAdmin = () => SetMetadata('SUPER_ADMIN', true)Extend the AuthGuard to check admin permissions:
import type { CanActivate, ExecutionContext } from '@nestjs/common'import { EntityManager } from '@mikro-orm/postgresql'import { Injectable, UnauthorizedException } from '@nestjs/common'import { Reflector } from '@nestjs/core'import { fromNodeHeaders } from 'better-auth/node'import { BetterAuthSession } from 'src/config/better-auth.config'import { AuthService } from 'src/modules/auth/auth.service'
export interface AuthenticatedRequest extends Request { session: NonNullable<BetterAuthSession>}
@Injectable()export class AuthGuard implements CanActivate { constructor( private readonly reflector: Reflector, private readonly authService: AuthService, private readonly em: EntityManager, ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { try { const request = context.switchToHttp().getRequest() const session = await this.authService.api.getSession({ headers: fromNodeHeaders(request.headers), })
request.session = session request.user = session?.user ?? null
// If user is super admin, skip all guards const isAdmin = session?.user?.role === 'admin' if (isAdmin) { return true }
// Public routes guard const isPublic = this.reflector.get('PUBLIC', context.getHandler()) if (isPublic) { return true }
// Super admin guard const superAdminGuard = this.reflector.get('SUPER_ADMIN', context.getHandler()) if (superAdminGuard) { if (!session) { throw new UnauthorizedException('Authentication required') } if (!isAdmin) { throw new UnauthorizedException('Admin access required') } }
// Default: require authentication if (!session) { throw new UnauthorizedException() }
return true } catch (error) { console.error(error) throw new UnauthorizedException() } }}Usage in your controllers:
import { Controller, Get, Post, Body, Param } from '@nestjs/common'import { SuperAdmin } from './auth/auth.decorator'
@Controller('admin')export class AdminController { @Get('users') @SuperAdmin() async listUsers() { // Only admins can access this endpoint return this.userService.findAll() }
@Post('users/:id/ban') @SuperAdmin() async banUser( @Param('id') userId: string, @Body() dto: BanUserDto ) { // Only admins can ban users return this.userService.ban(userId, dto) }}Frontend admin UI examples
Section titled “Frontend admin UI examples”Here are some common patterns for building admin interfaces:
Admin check:
import { useAuth } from '@/lib/auth'
export function AdminPanel() { const { user } = useAuth()
if (user?.role !== 'admin') { return <div>Access denied</div> }
return ( <div> <h1>Admin Panel</h1> {/* Admin features */} </div> )}