Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Roles

User roles and permissions

Overview

This template includes a role-based access control system using Better-Auth. Roles control access to routes, procedures, and UI elements.

Role Definitions

User roles are defined in src/db/enums.ts:

export enum UserRole {
  ADMIN = "admin",
  USER = "user",
}

Server-Side Role Checks

Layout Guards

Protect entire route groups at the layout level:

// app/[locale]/(admin)/layout.tsx
import { auth } from "@/lib/auth/server";
import { UserRole } from "@/db/enums";

export default async function AdminLayout({ children }) {
  // Require admin role
  await auth.requireRole(UserRole.ADMIN, { 
    preserveCallback: true 
  });
  
  return <AdminLayout>{children}</AdminLayout>;
}

Single Role

const session = await auth.requireRole(UserRole.ADMIN);

Multiple Roles

const session = await auth.requireRole([UserRole.ADMIN, UserRole.MODERATOR]);

With Options

await auth.requireRole(UserRole.ADMIN, {
  preserveCallback: true,              // Save redirect URL
  requireOnboardingCompleted: true,    // Require onboarding
});

Role-Based Procedures

Single Role

Require a specific role for a tRPC procedure:

import { roleProcedure } from "@/trpc/procedures/trpc";
import { UserRole } from "@/db/enums";

export const adminRouter = createTRPCRouter({
  listUsers: roleProcedure(UserRole.ADMIN).query(async ({ ctx }) => {
    return ctx.db.users.list();
  }),
});

Multiple Roles

Allow multiple roles:

export const moderationRouter = createTRPCRouter({
  moderateContent: roleProcedure([UserRole.ADMIN, UserRole.MODERATOR])
    .mutation(async ({ ctx, input }) => {
      // Available to both admins and moderators
      return await ctx.db.content.moderate(input);
    }),
});

Role Access in Procedures

Access user role in any auth procedure:

export const profileRouter = createTRPCRouter({
  getProfile: authProcedure.query(async ({ ctx }) => {
    const isAdmin = ctx.user.role === UserRole.ADMIN;
    
    if (isAdmin) {
      // Return additional data for admins
    }
    
    return { user: ctx.user, isAdmin };
  }),
});

Client-Side Role Checks

Client-side role checks are for UI purposes only. Always validate roles on the server.

In React Components

"use client";

import { useAuth } from "@/lib/auth/client";
import { UserRole } from "@/db/enums";

export function AdminPanel() {
  const { user } = useAuth();

  if (user?.role !== UserRole.ADMIN) {
    return <AccessDenied />;
  }

  return <AdminDashboard />;
}

Conditional Rendering

export function Navigation() {
  const { user } = useAuth();

  return (
    <nav>
      <Link href={PageHome()}>Home</Link>
      <Link href={PageDashboard()}>Dashboard</Link>
      
      {user?.role === UserRole.ADMIN && (
        <Link href={PageAdmin()}>Admin</Link>
      )}
    </nav>
  );
}

Role Helper

"use client";

import { useAuth } from "@/lib/auth/client";
import { UserRole } from "@/db/enums";

export function useHasRole(role: UserRole | UserRole[]) {
  const { user } = useAuth();
  
  if (!user) return false;
  
  if (Array.isArray(role)) {
    return role.includes(user.role);
  }
  
  return user.role === role;
}

// Usage
export function AdminButton() {
  const isAdmin = useHasRole(UserRole.ADMIN);
  
  if (!isAdmin) return null;
  
  return <Button>Admin Action</Button>;
}

Protected Routes

Middleware Protection

Routes are automatically protected based on PagesRequiringAuth:

// src/routes/routing.ts
export const routingConfig = {
  PagesRequiringAuth: () => [
    PageDashboard,
    PageSettingsProfile,
    PageAdmin, // Only admins can access
  ],
  // ...
};

Role-Specific Protection

For role-specific protection, use layout guards:

// app/[locale]/(admin)/layout.tsx
export default async function Layout({ children }) {
  await auth.requireRole(UserRole.ADMIN);
  return <>{children}</>;
}

Adding New Roles

Update Enum

Add the new role to src/db/enums.ts:

export enum UserRole {
  ADMIN = "admin",
  MODERATOR = "moderator",
  USER = "user",
}

Update Database

Push schema changes:

pnpm db:push

Update Type Definitions

TypeScript will automatically pick up the new role from the enum.

Use in Code

await auth.requireRole(UserRole.MODERATOR);

Role Hierarchy

You can implement role hierarchy by checking multiple roles:

// Helper function
function hasRoleOrHigher(userRole: UserRole, requiredRole: UserRole): boolean {
  const hierarchy = [UserRole.USER, UserRole.MODERATOR, UserRole.ADMIN];
  const userIndex = hierarchy.indexOf(userRole);
  const requiredIndex = hierarchy.indexOf(requiredRole);
  return userIndex >= requiredIndex;
}

// Usage in procedure
export const contentRouter = createTRPCRouter({
  delete: authProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      // Allow MODERATOR and ADMIN
      if (!hasRoleOrHigher(ctx.user.role, UserRole.MODERATOR)) {
        throw new TRPCError({ code: "FORBIDDEN" });
      }
      
      return await ctx.db.content.delete(input);
    }),
});

Best Practices

1. Always Validate on Server

// ❌ Bad - Client-only check
if (user?.role === "admin") {
  // Can be bypassed
  await deleteUser(id);
}

// ✅ Good - Server-side validation
export const adminRouter = createTRPCRouter({
  deleteUser: roleProcedure(UserRole.ADMIN).mutation(/* ... */),
});

2. Use Layout Guards

Protect entire sections:

// ✅ Good - Layout protection
export default async function AdminLayout({ children }) {
  await auth.requireRole(UserRole.ADMIN);
  return <>{children}</>;
}

3. Consistent Error Messages

if (ctx.user.role !== UserRole.ADMIN) {
  return {
    success: false,
    message: ctx.t("errors.insufficientPermissions"),
  };
}

4. Log Role Changes

export const userRouter = createTRPCRouter({
  updateRole: roleProcedure(UserRole.ADMIN)
    .input(z.object({
      userId: z.string(),
      newRole: z.nativeEnum(UserRole),
    }))
    .mutation(async ({ ctx, input }) => {
      // Log the role change
      console.log(`Admin ${ctx.user.id} changed role of ${input.userId} to ${input.newRole}`);
      
      return await ctx.db.users.update({
        where: { id: input.userId },
        data: { role: input.newRole },
      });
    }),
});

Common Patterns

Admin-Only Components

import { RoleGuard } from "@/components/auth/role-guard";
import { UserRole } from "@/db/enums";

<RoleGuard role={UserRole.ADMIN}>
  <AdminSettings />
</RoleGuard>

Multi-Level Access

export const dashboardRouter = createTRPCRouter({
  getData: authProcedure.query(async ({ ctx }) => {
    const baseData = await getBaseData();
    
    if (ctx.user.role === UserRole.ADMIN) {
      return {
        ...baseData,
        adminData: await getAdminData(),
      };
    }
    
    return baseData;
  }),
});

Next Steps

Session Management

Authorization Patterns

Protected Routes

On this page

Overview
Role Definitions
Server-Side Role Checks
Layout Guards
Single Role
Multiple Roles
With Options
Role-Based Procedures
Single Role
Multiple Roles
Role Access in Procedures
Client-Side Role Checks
In React Components
Conditional Rendering
Role Helper
Protected Routes
Middleware Protection
Role-Specific Protection
Adding New Roles
Update Enum
Update Database
Update Type Definitions
Use in Code
Role Hierarchy
Best Practices
1. Always Validate on Server
2. Use Layout Guards
3. Consistent Error Messages
4. Log Role Changes
Common Patterns
Admin-Only Components
Multi-Level Access
Next Steps