Action Middleware
Authentication and authorization middleware for server actions.
Middleware eliminates boilerplate from server actions. Instead of manually checking authentication in every action, you chain pre-built middleware that handles auth, permissions, and context injection. This keeps your action logic focused on business requirements.
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.update(users) .set({ name: parsedInput.name }) .where(eq(users.id, user.id)); 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 }, }); });Next: Working with Forms →