how to secure server actions the right way - By Sourav Mishra (@souravvmishra)
stop writing manual role checks. learn how to build a simple wrapper for your next.js server actions.
security in next.js server actions is super important. a lot of people just hide the delete button in the ui and call it a day.
but if someone finds the url, they can still trigger the action!
here's how i secure everything using a simple wrapper pattern.
the annoying manual way
export async function deleteProduct(id: string) {
const session = await auth();
if (!session || session.user.role !== 'ADMIN') {
throw new Error('nope');
}
// delete stuff
}
if you forget that if block just once, your app is completely open.
the wrapper pattern
let's make a wrapper function so we don't have to repeat ourselves.
1. make the wrapper
// lib/safe-action.ts
import { auth } from './auth';
type Role = 'ADMIN' | 'USER';
export function authAction(allowedRoles: Role[], action: Function) {
return async (...args: any[]) => {
const session = await auth();
if (!session?.user) throw new Error('not logged in');
if (!allowedRoles.includes(session.user.role)) throw new Error('not allowed');
return action(...args);
};
}
2. use it everywhere
now, wrapping your actions is super clean.
// actions/products.ts
import { authAction } from '@/lib/safe-action';
export const deleteProduct = authAction(['ADMIN'], async (id: string) => {
await db.product.delete({ where: { id } });
});
why this is better
- less typing: you only write the auth check once.
- safer: you can easily see who is allowed to do what.
- cleaner: your actual logic isn't cluttered with if statements.
pro tip: never trust the client! just because a button is hidden doesn't mean the server is safe.
wanna know more about security? read my middleware post.
written by sourav mishra, just trying not to get hacked.