Implementing Role-Based Access Control in Server Actions - By Sourav Mishra (@souravvmishra)
Stop manually checking user roles in every function. Learn how to build a type-safe RBAC wrapper for your Next.js Server Actions.
Security in Next.js Server Actions is critical. A common mistake is relying on the UI to hide buttons, but forgetting to secure the actual server function.
In this guide, I, Sourav Mishra, Co-founder of Codestam Technologies, share my preferred pattern for robust Role-Based Access Control (RBAC) that protects all our enterprise applications.
The Manual Way (Repetitive & Unsafe)
export async function deleteProduct(id: string) {
const session = await auth();
if (!session || session.user.role !== 'ADMIN') {
throw new Error('Unauthorized');
}
// ... delete logic
}
If you forget that if block once, your app is vulnerable.
The Higher-Order Function (HOF) Pattern
Let's create a wrapper that handles this logic centrally.
1. Create the Wrapper
// lib/safe-action.ts
import { auth } from './auth';
type Role = 'ADMIN' | 'USER';
export function authenticatedAction(
allowedRoles: Role[],
action: Function
) {
return async (...args: any[]) => {
const session = await auth();
if (!session?.user) {
throw new Error('Not authenticated');
}
if (!allowedRoles.includes(session.user.role)) {
throw new Error('Forbidden');
}
return action(...args);
};
}
2. Use It Everywhere
Now, writing secure actions is cleaner and safer.
// actions/products.ts
import { authenticatedAction } from '@/lib/safe-action';
export const deleteProduct = authenticatedAction(
['ADMIN'],
async (id: string) => {
await db.product.delete({ where: { id } });
}
);
At Codestam Technologies, we take this a step further by defining granular permissions (e.g., product:delete, user:view) instead of just roles. This allows us to create custom roles for clients without changing the code.
Benefits
- D.R.Y (Don't Repeat Yourself): Auth logic is written once.
- Auditability: You can see exactly which potential roles access which function.
- Type Safety: You can extend the HOF to infer input types (using Zod).
Key Takeaways
- Never trust the client: Just because a button is hidden doesn't mean the action is unreachable.
- Centralize Auth: Use wrappers/middleware logic for consistency.
For more security tips, ensure your middleware isn't leaking data by reading Middleware Anti-Patterns.
This guide was written by Sourav Mishra, Co-founder of Codestam Technologies and a Full Stack Engineer.