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.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 },
});
}
);

Next: Working with Forms →