Teach server action authentication and security patterns in Next.js 16. Use when implementing server actions, form handlers, or mutations that need authentication.
Teaches authentication, authorization, and validation patterns for Next.js server actions. Use when implementing secure form handlers or mutations that require session verification, permission checks, and input validation.
/plugin marketplace add djankies/claude-configs/plugin install nextjs-16@claude-configsThis skill is limited to using the following tools:
Secure server actions with authentication, authorization, and validation.
Every server action must verify the session before processing:
'use server'
import { verifySession } from '@/lib/dal'
import { z } from 'zod'
const updateProfileSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email()
})
export async function updateProfile(formData: FormData) {
const session = await verifySession()
if (!session) {
return { error: 'Unauthorized' }
}
const validatedFields = updateProfileSchema.safeParse({
name: formData.get('name'),
email: formData.get('email')
})
if (!validatedFields.success) {
return {
error: 'Validation failed',
fields: validatedFields.error.flatten().fieldErrors
}
}
const { name, email } = validatedFields.data
await db.user.update({
where: { id: session.userId },
data: { name, email }
})
return { success: true }
}
Verify user permissions beyond authentication:
'use server'
import { verifySession } from '@/lib/dal'
import { z } from 'zod'
const deletePostSchema = z.object({
postId: z.string().uuid()
})
export async function deletePost(formData: FormData) {
const session = await verifySession()
if (!session) {
return { error: 'Unauthorized' }
}
const validatedFields = deletePostSchema.safeParse({
postId: formData.get('postId')
})
if (!validatedFields.success) {
return { error: 'Invalid post ID' }
}
const post = await db.post.findUnique({
where: { id: validatedFields.data.postId }
})
if (!post) {
return { error: 'Post not found' }
}
if (post.authorId !== session.userId && session.role !== 'admin') {
return { error: 'Forbidden: You cannot delete this post' }
}
await db.post.delete({
where: { id: validatedFields.data.postId }
})
return { success: true }
}
Implement defense in depth with multiple security layers:
'use server'
import { verifySession } from '@/lib/dal'
import { z } from 'zod'
import { rateLimit } from '@/lib/rate-limit'
const transferFundsSchema = z.object({
toUserId: z.string().uuid(),
amount: z.number().positive().max(10000)
})
export async function transferFunds(formData: FormData) {
const session = await verifySession()
if (!session) {
return { error: 'Unauthorized' }
}
const rateLimitResult = await rateLimit(session.userId, 'transfer', {
max: 5,
window: '1h'
})
if (!rateLimitResult.success) {
return {
error: 'Rate limit exceeded',
retryAfter: rateLimitResult.retryAfter
}
}
const validatedFields = transferFundsSchema.safeParse({
toUserId: formData.get('toUserId'),
amount: Number(formData.get('amount'))
})
if (!validatedFields.success) {
return {
error: 'Validation failed',
fields: validatedFields.error.flatten().fieldErrors
}
}
const { toUserId, amount } = validatedFields.data
if (toUserId === session.userId) {
return { error: 'Cannot transfer to yourself' }
}
const balance = await db.account.findUnique({
where: { userId: session.userId },
select: { balance: true }
})
if (!balance || balance.balance < amount) {
return { error: 'Insufficient funds' }
}
await db.$transaction([
db.account.update({
where: { userId: session.userId },
data: { balance: { decrement: amount } }
}),
db.account.update({
where: { userId: toUserId },
data: { balance: { increment: amount } }
})
])
return { success: true }
}
If implementing production transaction error handling with P-code detection, timeout configuration, and retry strategies, use the handling-transaction-errors skill from prisma-6 for comprehensive patterns beyond this basic example.
For comprehensive Zod validation patterns and runtime type checking, use the using-runtime-checks skill from the typescript plugin.
Structure validation schemas for reusability:
'use server'
import { verifySession } from '@/lib/dal'
import { z } from 'zod'
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1).max(50000),
tags: z.array(z.string()).max(5).optional(),
published: z.boolean().default(false)
})
type CreatePostInput = z.infer<typeof createPostSchema>
export async function createPost(formData: FormData) {
const session = await verifySession()
if (!session) {
return { error: 'Unauthorized' }
}
const tags = formData.getAll('tags').filter(Boolean) as string[]
const validatedFields = createPostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
tags: tags.length > 0 ? tags : undefined,
published: formData.get('published') === 'true'
})
if (!validatedFields.success) {
return {
error: 'Validation failed',
fields: validatedFields.error.flatten().fieldErrors
}
}
const post = await createPostInDb(session.userId, validatedFields.data)
return { success: true, postId: post.id }
}
async function createPostInDb(userId: string, data: CreatePostInput) {
return db.post.create({
data: {
...data,
authorId: userId
}
})
}
Every server action must implement:
verifySession() firstFor comprehensive form state management patterns and action state handling, use the using-action-state skill from the react-19 plugin.
Use with React 19's useActionState hook for form state management:
'use client'
import { useActionState } from 'react'
import { updateProfile } from './actions'
export function ProfileForm() {
const [state, action, isPending] = useActionState(updateProfile, null)
return (
<form action={action}>
<input name="name" required />
{state?.fields?.name && <span>{state.fields.name[0]}</span>}
<input name="email" type="email" required />
{state?.fields?.email && <span>{state.fields.email[0]}</span>}
{state?.error && <div>{state.error}</div>}
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
)
}
Zod v4 Validation:
Prisma 6 Integration:
Avoid these security mistakes:
export async function badAction(formData: FormData) {
const postId = formData.get('postId') as string
await db.post.delete({ where: { id: postId } })
return { success: true }
}
Problems:
Correct implementation:
'use server'
import { verifySession } from '@/lib/dal'
import { z } from 'zod'
const deletePostSchema = z.object({
postId: z.string().uuid()
})
export async function deletePost(formData: FormData) {
const session = await verifySession()
if (!session) {
return { error: 'Unauthorized' }
}
const validatedFields = deletePostSchema.safeParse({
postId: formData.get('postId')
})
if (!validatedFields.success) {
return { error: 'Invalid input' }
}
const post = await db.post.findUnique({
where: { id: validatedFields.data.postId }
})
if (!post) {
return { error: 'Post not found' }
}
if (post.authorId !== session.userId) {
return { error: 'Forbidden' }
}
await db.post.delete({
where: { id: validatedFields.data.postId }
})
return { success: true }
}
Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.