Action Middleware
Configure authentication and authorization middleware for server actions in your Next.js Prisma kit application.
Enforce authentication and authorization declaratively with action middleware - pre-built chains that verify sessions, check permissions, and inject context before your server action runs.
Action middleware in the Next.js Prisma kit eliminates repetitive auth checks. Instead of manually verifying sessions in every action, you choose the right action client: authenticatedActionClient for logged-in users, adminActionClient for super-admins, or chain .use(withFeaturePermission()) for granular permission checks. The middleware runs before your action and throws if checks fail.
Action middleware is a chain of functions that run before your server action. Each middleware can verify conditions, inject context, or reject the request.
- Use
authenticatedActionClientwhen: you need to verify the user is logged in and access their session data. - Use
adminActionClientwhen: the operation is super-admin only (banning users, accessing all accounts). - Use
withFeaturePermissionwhen: you need to verify the user can perform a specific action on a resource.
Action Clients
Action clients are pre-configured with middleware chains. Choose the right client based on what your action requires.
authenticatedActionClient
The most common choice. This client verifies the user has a valid session before your action runs:
import { authenticatedActionClient } from '@kit/action-middleware';export const myAction = authenticatedActionClient .inputSchema(mySchema) .action(async ({ parsedInput, ctx }) => { const { user } = ctx; // Authenticated user // Action logic });The middleware automatically:
- Verifies the session token is valid
- Throws an error if the user isn't authenticated
- Injects the authenticated user into
ctx
adminActionClient
For super-admin operations like banning users or accessing any account's data. This client adds an admin role check on top of authentication:
import { adminActionClient } from '@kit/action-middleware';export const myAdminAction = adminActionClient .inputSchema(mySchema) .action(async ({ parsedInput, ctx }) => { const { user } = ctx; // Verified admin user // Admin-only logic });The middleware:
- Verifies the user session
- Confirms the user has admin privileges
- Throws immediately if either check fails
Usage Examples
Basic Authenticated Action
Most actions follow this pattern - validate input with a Zod schema and perform the operation:
'use server';import { z } from 'zod';import { authenticatedActionClient } from '@kit/action-middleware';const updateProfileSchema = z.object({ name: z.string().min(1),});export const updateProfileAction = authenticatedActionClient .inputSchema(updateProfileSchema) .action(async ({ parsedInput, ctx }) => { const { user } = ctx; await db.user.update({ where: { id: user.id }, data: { name: parsedInput.name }, }); return { success: true }; });With Permission Check
For organization-scoped actions, use withFeaturePermission to verify the user can perform the operation. This middleware checks the user's permissions using Better Auth and injects organization context:
import { authenticatedActionClient, withFeaturePermission } from '@kit/action-middleware';export const deleteProjectAction = authenticatedActionClient .use(withFeaturePermission({ project: ['delete'] })) .inputSchema(z.object({ projectId: z.string() })) .action(async ({ parsedInput, ctx }) => { // Permission already verified by middleware // ctx.organizationId is available // Delete project });Alternatively, withMinRole checks role hierarchy directly. Use this when you need "admin or higher" rather than specific permissions:
import { authenticatedActionClient, withMinRole } from '@kit/action-middleware';export const adminAction = authenticatedActionClient .use(withMinRole('admin')) .inputSchema(schema) .action(async ({ parsedInput, ctx }) => { // Only admins and owners can reach here });Admin Action
Admin actions bypass organization context entirely - they operate at the platform level:
import { adminActionClient } from '@kit/action-middleware';export const banUserAction = adminActionClient .inputSchema(z.object({ userId: z.string(), reason: z.string().min(10), })) .action(async ({ parsedInput, ctx }) => { // Only admins reach here await auth.api.banUser({ body: { userId: parsedInput.userId, banReason: parsedInput.reason, }, }); });Error Handling
When middleware rejects a request (invalid session, missing permission), the action throws. On the client, check the result for errors:
'use client';import { useAction } from 'next-safe-action/hooks';import { myAction } from './actions';function MyComponent() { const { execute, status, result } = useAction(myAction); const handleSubmit = async (data) => { const result = await execute(data); if (result?.serverError) { // Handle auth or other errors console.error(result.serverError); } };}Context Properties
Middleware enriches the ctx object passed to your action. The exact properties depend on which middleware ran:
interface ActionContext { user: { id: string; email: string; name: string; role?: string; // ... other Better Auth user fields };}Creating Custom Middleware
Build custom middleware when you need to add context or perform checks not covered by the built-in options. Middleware receives the current context and returns an enriched version:
import { authenticatedActionClient } from '@kit/action-middleware';export const orgMemberActionClient = authenticatedActionClient.use( async ({ next, ctx }) => { const { user } = ctx; // Add custom validation or context enrichment const userProfile = await getUserProfile(user.id); if (!userProfile) { throw new Error('Profile not found'); } return next({ ctx: { ...ctx, profile: userProfile }, }); });Common Pitfalls
- Wrong action client: Using
authenticatedActionClientwhen you need organization context. UseorganizationActionClientor addwithFeaturePermission. - Swallowing middleware errors: When middleware throws, the error should propagate. Don't wrap in try/catch unless you're enriching the error.
- Expensive middleware: Middleware runs on every request. Avoid database calls in middleware unless absolutely necessary - prefer checking at the action level.
- Forgetting to return
next(): Custom middleware must call and returnnext()to continue the chain. Forgetting this silently breaks the action. - Hardcoding roles: Use
withFeaturePermissionfor resource-based checks instead of checkingctx.role === 'admin'directly.
Frequently Asked Questions
What's the difference between withMinRole and withFeaturePermission?
Can I use multiple middleware?
How do I create organization-scoped actions?
What happens when middleware throws?
Related
- Development Guide Overview - Full development patterns guide
- Server Actions - Type-safe mutations with authentication
- Working with Forms - Client-side form integration
Next: Working with Forms →