1. Server Actions treated as private functions
Symptom
Nothing looks wrong. The action is only imported by one form, and that form only renders for admins.
The pattern
"use server";
export async function deleteProject(projectId: string) {
await db.project.delete({ where: { id: projectId } });
revalidatePath("/projects");
}Why it bites
`"use server"` publishes the function as an RPC endpoint with a stable, guessable action ID. Whether the form renders is irrelevant — anyone can POST to it directly with any projectId. The import graph is not an access control list.
The fix
"use server";
export async function deleteProject(projectId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
// Authorize the *object*, not just the caller.
const project = await db.project.findUnique({
where: { id: projectId },
select: { ownerId: true },
});
if (project?.ownerId !== session.user.id) throw new Error("Forbidden");
await db.project.delete({ where: { id: projectId } });
revalidatePath("/projects");
}