Overview
AiVault implements Role-Based Access Control (RBAC) through a whitelist-based admin system. Only users whose Clerk User IDs are explicitly listed in the admin whitelist can access administrative functions.
Admin Whitelist Configuration
Environment Variable Setup
Admin access is controlled via the NEXT_PUBLIC_ADMIN_USER_IDS environment variable:
# Admin User IDs (comma-separated Clerk user IDs)
# Users listed here can access the /admin dashboard to approve/reject tool submissions
NEXT_PUBLIC_ADMIN_USER_IDS=user_2th1234567890,user_2th0987654321
Never commit actual admin user IDs to version control. Use .env.local for local development and secure environment variable management in production.
Multiple Admin Support
The whitelist supports multiple administrators:
Comma-separated list of Clerk user IDs
Automatically trims whitespace
Filters out empty entries
No limit on number of admins
Server-Side Implementation
Convex Admin Check
The checkAdmin function in convex/tools.ts enforces admin access:
const getAdminIds = () => ( process . env . NEXT_PUBLIC_ADMIN_USER_IDS || "" )
. split ( "," )
. map (( id ) => id . trim ())
. filter ( Boolean );
async function checkAdmin ( ctx : QueryCtx | MutationCtx ) {
const identity = await ctx . auth . getUserIdentity ();
if ( ! identity ) throw new Error ( "Unauthenticated" );
if ( ! getAdminIds (). includes ( identity . subject )) {
throw new Error ( "Unauthorized: Admin access required" );
}
return identity ;
}
Key Security Features
Identity Verification
Uses ctx.auth.getUserIdentity() to get the authenticated user from Clerk’s JWT token
Authentication Check
Throws "Unauthenticated" error if no valid identity is found
Authorization Check
Compares identity.subject (Clerk User ID) against the admin whitelist
Error Handling
Throws "Unauthorized: Admin access required" if user is not in whitelist
Protected Admin Operations
The following Convex functions require admin privileges:
export const getPendingTools = query ({
handler : async ( ctx : QueryCtx ) => {
await checkAdmin ( ctx );
return await ctx . db
. query ( "tools" )
. withIndex ( "by_approved" , ( q ) => q . eq ( "approved" , false ))
. collect ();
},
});
export const approveTool = mutation ({
args: {
toolId: v . id ( "tools" ),
sendEmail: v . optional ( v . boolean ()),
},
handler : async ( ctx : MutationCtx , args ) => {
await checkAdmin ( ctx );
await ctx . db . patch ( args . toolId , { approved: true });
return { success: true };
},
});
export const rejectTool = mutation ({
args: {
toolId: v . id ( "tools" ),
reason: v . optional ( v . string ()),
sendEmail: v . optional ( v . boolean ()),
},
handler : async ( ctx : MutationCtx , args ) => {
await checkAdmin ( ctx );
const tool = await ctx . db . get ( args . toolId );
await ctx . db . delete ( args . toolId );
return { success: true , tool , reason: args . reason };
},
});
Get Admin Statistics
export const getAdminStats = query ({
handler : async ( ctx : QueryCtx ) => {
await checkAdmin ( ctx );
const approvedTools = await ctx . db
. query ( "tools" )
. withIndex ( "by_approved" , ( q ) => q . eq ( "approved" , true ))
. collect ();
const pendingTools = await ctx . db
. query ( "tools" )
. withIndex ( "by_approved" , ( q ) => q . eq ( "approved" , false ))
. collect ();
return {
totalTools: approvedTools . length + pendingTools . length ,
approvedCount: approvedTools . length ,
pendingCount: pendingTools . length ,
// ... more stats
};
},
});
Client-Side Admin Check
For UI elements, a client-side helper function is available in lib/admin.ts:
const adminIds = ( process . env . NEXT_PUBLIC_ADMIN_USER_IDS || "" )
. split ( "," )
. map (( id ) => id . trim ())
. filter ( Boolean );
export function isAdmin ( userId : string | null | undefined ) : boolean {
if ( ! userId ) return false ;
return adminIds . includes ( userId );
}
This client-side check is for UI purposes only (showing/hiding admin buttons). Real security is always enforced server-side in Convex functions.
Route Protection
The /admin route is protected via Clerk middleware in middleware.ts:
const isProtectedRoute = createRouteMatcher ([
"/dashboard(.*)" ,
"/submit(.*)" ,
"/admin(.*)" ,
]);
export default clerkMiddleware ( async ( auth , req ) => {
if ( isProtectedRoute ( req )) {
await auth . protect ();
}
} ) ;
This ensures users must be authenticated before accessing admin pages, with additional authorization checks performed by Convex.
Finding Your Clerk User ID
To add yourself or others as an admin:
Sign in to your application
Authenticate using Clerk in your running AiVault instance
Find User ID
Navigate to Users section and copy the User ID (starts with user_)
Update Environment Variable
Add the User ID to NEXT_PUBLIC_ADMIN_USER_IDS in your .env.local file
Restart Development Server
Restart both Next.js and Convex dev servers to apply changes
Security Considerations
Critical Security Points:
Admin checks run on every admin operation
Identity derived from server-side Clerk tokens (cannot be spoofed)
Whitelist stored in environment variables (never in client code)
No “temporary” or “guest” admin access
User IDs are permanent and cannot be changed by users
Best Practices
Principle of Least Privilege Only grant admin access to users who absolutely need it. Regularly audit your admin list.
Secure Environment Variables Use secure secret management in production (Vercel Environment Variables, AWS Secrets Manager, etc.)
Monitor Admin Actions Consider adding audit logging for all admin operations (tool approvals, rejections, etc.)
Separate Staging Admins Use different admin user lists for development, staging, and production environments
Troubleshooting
”Unauthorized: Admin access required” Error
Verify your User ID is correct (check Clerk Dashboard)
Ensure NEXT_PUBLIC_ADMIN_USER_IDS is set correctly
Restart both Next.js and Convex dev servers
Clear browser cache and re-authenticate
Check for typos or extra whitespace in user IDs
Admin Functions Not Working
Confirm you’re authenticated (signed in)
Check that Convex deployment has the correct environment variable
Verify ctx.auth is properly configured in Convex
Check browser console for specific error messages
Next Steps
Security Overview Learn about AiVault’s overall security architecture
Best Practices Security best practices for production deployment