# Action Middleware

> Configure authentication and authorization middleware for server actions in your Next.js Prisma kit application.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/development-guide/action-middleware*

---

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 `authenticatedActionClient` when:** you need to verify the user is logged in and access their session data.
- **Use `adminActionClient` when:** the operation is super-admin only (banning users, accessing all accounts).
- **Use `withFeaturePermission` when:** 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:

```typescript
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:

```typescript
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:

```typescript
'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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
'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:

```typescript
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:

```typescript
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 `authenticatedActionClient` when you need organization context. Use `organizationActionClient` or add `withFeaturePermission`.
- **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 return `next()` to continue the chain. Forgetting this silently breaks the action.
- **Hardcoding roles**: Use `withFeaturePermission` for resource-based checks instead of checking `ctx.role === 'admin'` directly.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "What's the difference between withMinRole and withFeaturePermission?", "answer": "withMinRole checks role hierarchy (admin can do what member can). withFeaturePermission checks specific permissions on resources (project:delete). Use withMinRole for broad access, withFeaturePermission for granular control."},
     {"question": "Can I use multiple middleware?", "answer": "Yes. Chain them with .use(): authenticatedActionClient.use(middleware1).use(middleware2). They run in order, and each can enrich ctx."},
     {"question": "How do I create organization-scoped actions?", "answer": "Use organizationActionClient or add withFeaturePermission to authenticatedActionClient. Both inject ctx.organizationId and verify membership."},
     {"question": "What happens when middleware throws?", "answer": "The action stops immediately and the error propagates to the client as result.serverError. The client should check for this and show an appropriate message."}
   ]
/%}

## Related

- [Development Guide Overview](./overview) - Full development patterns guide
- [Server Actions](./server-actions) - Type-safe mutations with authentication
- [Working with Forms](./working-with-forms) - Client-side form integration

---

**Next:** [Working with Forms →](./working-with-forms)
