Server Actions reachability and security concerns
By default, when a Server Action is created and exported, it is reachable via a direct POST request, not just through the application UI. This means even if a Server Action is not imported elsewhere in the code, it can still be called externally. You should treat Server Actions as reachable via direct POST requests and verify authentication and authorization inside each one.
Next.js built-in Server Actions security features
Next.js provides two built-in security features for Server Actions: (1) Secure action IDs - encrypted, non-deterministic IDs created during compilation that are periodically recalculated between builds. (2) Dead code elimination - unused Server Actions are removed from the client bundle. IDs are cached for a maximum of 14 days and regenerated when a new build is initiated or the build cache is invalidated.
Validating client input in Server Actions and components
Always validate input from the client as it can be easily modified. This includes form data, URL parameters, headers, and searchParams. Do not trust client-provided values directly; re-verify sensitive information like admin status by checking server-side data such as authentication tokens, not URL parameters.
Re-verifying authentication in Server Actions
Page-level authentication checks do not extend to Server Actions defined within them. You must always re-verify authentication and authorization inside each Server Action, even if the page level checks have passed. This ensures the action can only be invoked by properly authenticated users with the correct permissions.
Authorization checks for resource ownership in Server Actions
In addition to checking authentication (is the user logged in?), always check authorization (does this user have permission to act on this specific resource?). This prevents Insecure Direct Object Reference (IDOR) vulnerabilities. Before performing an action on a resource, verify the current user owns or has permission to access that specific resource.
Server Action authorization check example
Example of checking both authentication and authorization in a Server Action: query the resource, verify the user is authenticated, then check if post.authorId === session.user.id before allowing deletion. Throw 'Unauthorized' error if not authenticated, 'Forbidden' error if authorized user doesn't own the resource.
Data Access Layer pattern for mutations
Apply the Data Access Layer pattern to mutations as well as reads. Keep authentication, authorization, and database logic in a dedicated server-only module, while 'use server' actions stay thin and delegate to the DAL. The DAL handles all security checks before mutations occur.
Server Action return value security
Server Action return values are serialized and sent to the client. Only return what the UI needs, not raw database records. Return minimal data objects with only the fields necessary for the client, not full database records that may contain internal fields the client should not see.
Rate limiting for expensive operations
For expensive operations like sending emails or writing to a database, consider adding rate limiting to prevent abuse. This is mentioned as a Backend for Frontend pattern in the guides.
Server Action closures capture variables for encryption
When a Server Action is defined inside a component, it creates a closure with access to the outer function's scope. Variables captured in the closure (e.g., publishVersion) are sent to the client and back when the action is invoked. Next.js automatically encrypts these closed-over variables to prevent sensitive data exposure. A new private key is generated for each action every time the Next.js application is built, meaning actions can only be invoked for a specific build.
CSRF protection for Server Actions
Server Actions are protected against CSRF attacks through multiple mechanisms: they use the POST HTTP method (only this method is allowed to invoke them, preventing most CSRF vulnerabilities), and they compare the Origin header to the Host header (or X-Forwarded-Host). If these headers don't match, the request is aborted. Server Actions can only be invoked on the same host as the page that hosts them.
Avoiding mutations during rendering in Next.js
Mutations such as logging out users, updating databases, or invalidating caches should never be side-effects during rendering in either Server or Client Components. Next.js explicitly prevents setting cookies or triggering cache revalidation within render methods to avoid unintended side effects. Use Server Actions to handle all mutations instead.
CSRF prevention through POST method in Server Actions
Next.js uses POST requests to handle mutations in Server Actions. This prevents accidental side-effects from GET requests, reducing Cross-Site Request Forgery (CSRF) risks since GET requests should be idempotent.
What is a Server Action
A Server Action is a React Server Function invoked through React's action mechanisms, such as <form action>, <button formAction>, or a client-side transition. You create one by adding the 'use server' directive, then invoke it from a form, or from an event handler or useEffect wrapped in startTransition.
Sequential dispatch of Server Actions on the client
Next.js dispatches Server Actions one at a time per client. If a user triggers three actions in quick succession, the second waits for the first to finish, then the third waits for the second. This keeps the re-rendered server tree consistent with the action result that produced it. Do not rely on Promise.all to parallelize Server Actions from the client. If you need parallel work, do it inside a single Server Action, fetch in parallel from a Server Component, or use a Route Handler for non-mutation requests.
Single response carries both data and UI from Server Action
When a Server Action triggers an immediate revalidation, Next.js runs the action and re-renders the current route server-side within one HTTP request. The response contains both the action's return value (consumed by useActionState or the awaited promise on the client) and a newly rendered RSC Payload for the current route, which the client commits as a seeded navigation. Your application does not need a follow-up fetch to see the updated UI for the current page.
When Server Action response includes a re-render
A re-render is included in the same response when the action calls updateTag, revalidatePath, refresh, mutates cookies through cookies(), or calls redirect. When updateTag, revalidatePath, or refresh runs, Next.js re-renders the current route server-side and includes a newly rendered RSC Payload in the action's response. revalidateTag with a stale-while-revalidate profile is the exception: it marks the tag for background refresh and does not include a re-render in the action response. The page reflects the change on a later read.
Server Action single roundtrip example with revalidatePath
The following example shows a Server Action that creates a post, revalidates the cache, and re-renders all in one roundtrip:
```ts
'use server'
import { revalidatePath } from 'next/cache'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
await db.post.create({
data: {
title: String(formData.get('title')),
authorId: session.user.id,
},
})
revalidatePath('/posts')
}
```
redirect after revalidation in Server Actions
Because redirect throws a control-flow exception, any code after it does not run. Place revalidation calls before redirect if the destination needs the fresh data.
Revalidation functions do not throw in Server Actions
Unlike redirect, updateTag, revalidateTag, revalidatePath, and refresh do not throw, so an action can call them and still return a value to the caller.
How Server Action is compiled to a reference
At build time, the 'use server' directive tells the compiler to swap the function's implementation in client bundles for a reference (an action ID plus a dispatcher) that POSTs back to the server. The implementation stays on the server, but the route is reachable to anyone who can send the same POST. Every action should be treated as an untrusted entry point.