'use server' marks server code. On the first line of a file, every export becomes a Server Action; on the first line of an async function in a Server Component, only that function does. Prefer the file form, which a Client Component can import from — it can never define one of its own.
"use server";
import { revalidatePath } from "next/cache";
// currentUser() reads the session cookie; addNote() writes to the store
export async function createNote(prevState, formData) {
const user = await currentUser();
console.log("[action] createNote by", user?.name ?? "anonymous");
if (user?.role !== "editor") return { error: "You may not add notes." };
await new Promise((r) => setTimeout(r, 900)); // stand in for a slow database
const note = addNote(String(formData.get("text")).trim(), user.id);
revalidatePath("/notes");
return { error: null, saved: note.text };
}Two rules are compiler-enforced: the function must be async, because calling it crosses a network, and its arguments and return value must serialize by the same rules as props (Serializing Props) — a Date and a Map survive, a class instance and a Mongoose 243,355 document do not.
Submit the form and the log appears in the terminal running next dev, never in the browser:
[action] createNote by Ada
POST /notes 200 in 951ms (next.js: 8ms, application-code: 943ms)
└─ ƒ createNote({"error":null}, {}) in 914ms actions.jsSearch the client bundle for that function body and you will not find it: Turbopack 10,514 emits createServerReference("6064...e653", callServer, ...) in its place, and the dispatcher around it POSTs to the current URL with that action ID in a Next-Action header. The action is an endpoint, then — as curl 3,008 shows, with no browser, form or cookie:
curl -X POST http://localhost:3507/notes -H "Content-Type: text/plain" \
-H "Next-Action: 60642d5789a87b0aae1e954fd2e8a9c4856dc7e653" --data '[{},{}]'0:{"a":"$@1","f":"","q":"","i":true,"b":"development"}
1:D"$2"
1:{"error":"You may not add notes."}The action ran. What stopped it was the role check inside the function, not the absence of a form for anonymous visitors. Rendering a form only for people allowed to use it is a courtesy, and the Next.js 10,514 documentation calls it no security boundary: every action needs its own session lookup and ownership check.
| Guard | Default | Where to change it |
|---|---|---|
| Origin vs Host check | On | serverActions.allowedOrigins |
| Request body limit | 1 MB | serverActions.bodySizeLimit |
| Closure variables encrypted | On | NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
The first is a free CSRF check: a POST carrying Origin: https://evil.example.com is aborted with "Invalid Server Actions request" before your code runs. Both config keys live under experimental. Action IDs are encrypted and unused actions stripped at build, so a function nobody calls has no endpoint; set the key wherever more than one instance runs, or instances cannot decrypt each other's references.