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 adminSecurity Best Practices
- Always validate roles on the backend - Frontend role checks are for UI only
- Use the principle of least privilege - Only grant necessary roles
- Audit role changes - Consider logging when roles are modified
- Regular reviews - Periodically review user roles and permissions
Extending the System
Adding New Roles
- Add the new role to the
ROLESconstant inroleHelpers.ts:
export const ROLES = {
USER: "user",
ADMIN: "admin",
SUPER_ADMIN: "super_admin",
MODERATOR: "moderator", // New role
} as const;- Update your UI and backend logic to handle the new role
- 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
- Create test users with different roles
- Test that each role can only access appropriate features
- Verify that role changes take effect immediately
- 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