Skip to content

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.

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/
├── modules/auth/
│ ├── auth.entity.ts (to update)
│ └── auth.config.ts (to update)

  1. Add the plugin to Better Auth configuration

    Section titled “Add the plugin to Better Auth configuration”

    In your Better Auth configuration file apps/api/src/modules/auth/auth.config.ts on the API side, add the admin plugin:

    import { admin } from 'better-auth/plugins'
    export const auth = betterAuth({
    // ... your existing config
    plugins: [
    admin({
    defaultRole: 'user',
    adminRoles: ['admin'],
    }),
    ],
    })

  2. The Admin plugin adds several fields to the user table. Update your User entity in auth.entity.ts:

    apps/api/src/modules/auth/auth.entity.ts
    @Entity({ tableName: '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 impersonatedBy field to track when an admin is impersonating another user:

    apps/api/src/modules/auth/auth.entity.ts
    @Entity({ tableName: 'session' })
    export class Session {
    // ... your existing properties
    @Property({ nullable: true })
    impersonatedBy!: string | null
    // ... your existing properties
    }

  3. 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.


  4. 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.

With @thallesp/nestjs-better-auth, prefer the built-in @Roles() decorator (admin plugin user.role) instead of forking AuthGuard.

Protect admin routes:

import { Controller, Get, Post, Body, Param } from '@nestjs/common'
import { Roles, Session } from '@thallesp/nestjs-better-auth'
import { LoggedInBetterAuthSession } from '../../modules/auth/auth.config'
@Controller('admin')
export class AdminController {
@Get('users')
@Roles(['admin'])
async listUsers(@Session() session: LoggedInBetterAuthSession) {
// Only users with role "admin" can access this endpoint
return this.userService.findAll()
}
@Post('users/:id/ban')
@Roles(['admin'])
async banUser(@Param('id') userId: string, @Body() dto: BanUserDto) {
return this.userService.ban(userId, dto)
}
}

For organization-scoped roles, use @OrgRoles() / @RequireActiveOrg() from the same package (see the organizations addon).

You can still call Better Auth admin APIs via the injectable AuthService from @thallesp/nestjs-better-auth (authService.api / authService.instance).

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>
)
}