Role-Based Authentication Documentation

This document describes the role-based authentication system implemented in the Convex Auth setup.

Overview

The role-based authentication system allows you to control access to features and data based on user roles. Each user can have multiple roles, with a default role of "user" assigned to all new users.

Default Roles

  • user - Basic user role (assigned by default to all users)
  • admin - Administrative privileges
  • super_admin - Super administrative privileges

Implementation Details

Schema Changes

The users table has been extended with a roles field:

users: defineTable({
  // ... existing fields ...
  roles: v.optional(v.array(v.string())),
})

Automatic Role Assignment

When a new user is created, they are automatically assigned the "user" role through the auth callback:

// In auth.ts
async afterUserCreatedOrUpdated(ctx, { userId, existingUserId, profile }) {
  if (existingUserId) return;
  
  const user = await ctx.db.get(userId);
  if (user && (!user.roles || user.roles.length === 0)) {
    await ctx.db.patch(userId, {
      roles: ["user"],
    });
  }
}

Frontend Usage

useAuth Hook

The useAuth hook has been extended with role-checking functions:

const { user, hasRole, hasAnyRole, hasAllRoles } = useAuth();

// Check if user has a specific role
if (hasRole("admin")) {
  // Show admin content
}

// Check if user has any of the specified roles
if (hasAnyRole(["admin", "super_admin"])) {
  // Show content for any admin
}

// Check if user has all specified roles
if (hasAllRoles(["user", "admin"])) {
  // Show content requiring both roles
}

Example Component

import { useAuth } from "@/hooks/useAuth";

export function AdminPanel() {
  const { hasRole } = useAuth();
  
  if (!hasRole("admin")) {
    return <div>Access denied. Admin privileges required.</div>;
  }
  
  return (
    <div>
      {/* Admin-only content */}
    </div>
  );
}

Backend Usage

Role Helpers

The roleHelpers.ts file provides server-side functions for role management:

import { requireRole, requireAnyRole, userHasRole } from "./roleHelpers";

// In a mutation or query
export const adminOnlyMutation = mutation({
  handler: async (ctx, args) => {
    // Throws an error if user doesn't have admin role
    await requireRole(ctx, "admin");
    
    // Proceed with admin-only logic
  },
});

// Check role without throwing
export const conditionalQuery = query({
  handler: async (ctx, args) => {
    const isAdmin = await userHasRole(ctx, "admin");
    
    if (isAdmin) {
      // Return admin data
    } else {
      // Return regular data
    }
  },
});

Role Management

Open Role Management

Role management is now open to all authenticated users. Any logged-in user can:

  • View all users and their roles
  • Add roles to any user
  • Remove roles from any user (except the base "user" role)
  • View all available roles

Programmatic Role Management

Use the provided mutations to manage roles:

import { api } from "@workspace/convex/_generated/api";

// Add a role to a user
await convex.mutation(api.roleManagement.addRole, {
  userId: "user_id_here",
  role: "admin"
});

// Remove a role from a user
await convex.mutation(api.roleManagement.removeRole, {
  userId: "user_id_here",
  role: "admin"
});

// Set all roles for a user
await convex.mutation(api.roleManagement.setRoles, {
  userId: "user_id_here",
  roles: ["user", "admin"]
});

CLI Role Management

Use the CLI script for development and testing:

# List all users and their roles
npx convex run scripts/manageRoles:listUsers

# Add a role to a user
npx convex run scripts/manageRoles:addRole -- --email user@example.com --role admin

# Remove a role from a user
npx convex run scripts/manageRoles:removeRole -- --email user@example.com --role admin

# Set all roles for a user
npx convex run scripts/manageRoles:setRoles -- --email user@example.com --roles user admin

Security Best Practices

  1. Always validate roles on the backend - Frontend role checks are for UI only
  2. Use the principle of least privilege - Only grant necessary roles
  3. Audit role changes - Consider logging when roles are modified
  4. Regular reviews - Periodically review user roles and permissions

Extending the System

Adding New Roles

  1. Add the new role to the ROLES constant in roleHelpers.ts:
export const ROLES = {
  USER: "user",
  ADMIN: "admin",
  SUPER_ADMIN: "super_admin",
  MODERATOR: "moderator", // New role
} as const;
  1. Update your UI and backend logic to handle the new role
  2. Document the permissions associated with the new role

Custom Role Hierarchies

You can implement role hierarchies by extending the role helper functions:

const roleHierarchy = {
  super_admin: ["admin", "moderator", "user"],
  admin: ["moderator", "user"],
  moderator: ["user"],
  user: []
};

function hasRoleWithHierarchy(userRoles: string[], requiredRole: string): boolean {
  for (const userRole of userRoles) {
    if (userRole === requiredRole) return true;
    if (roleHierarchy[userRole]?.includes(requiredRole)) return true;
  }
  return false;
}

Testing Role-Based Features

  1. Create test users with different roles
  2. Test that each role can only access appropriate features
  3. Verify that role changes take effect immediately
  4. Test edge cases (no roles, multiple roles, invalid roles)

Troubleshooting

  • User has no roles: Check that the auth callback is running on user creation
  • Role not persisting: Ensure you're using the correct mutation to update roles
  • Frontend not reflecting role changes: The user may need to refresh their auth state
  • Backend rejecting valid roles: Verify the role exists in your ROLES constant

On this page