# Function Middleware

> Configure authentication and authorization middleware for server functions in your TanStack Start Drizzle SaaS application.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/development-guide/function-middleware*

---

Middleware eliminates boilerplate from server functions. Instead of manually checking authentication in every function, you attach a pre-composed authorization middleware tuple to a literal `createServerFn(...)` call, then layer extra gates as needed. This keeps your function logic focused on business requirements.

The package is built on TanStack Start's `createServerFn` + `createMiddleware`. It
lives at `packages/function-middleware` and exposes lower-level **middleware** and
permission gates via `@kit/function-middleware/server`, plus **pre-composed middleware
tuples** via `@kit/function-middleware/functions` you attach to a `createServerFn`
builder. The root `@kit/function-middleware` barrel re-exports only the three
middleware tuples.

Always call `createServerFn(...)` **directly** at the definition site and pass the
tuple to `.middleware(...)`. This is what lets TanStack Start's compiler detect the
server function and strip its handler — and the database/auth graph it pulls — from
the client bundle. A helper that _returns_ a `createServerFn` builder hides it from
the compiler and ships the handler to the browser.

## Middleware Tuples

Each tuple pre-binds `errorMiddleware` plus an auth middleware. Attach it to a
literal `createServerFn(...)` call (mutations default to `{ method: 'POST' }`) with
`.middleware(...)`, then continue the chain with `.validator(...)`, extra
`.middleware([...])` gates, and `.handler(...)`. The handler's `context` is fully
typed from the middleware.

### authFunctionMiddleware

The most common choice. Verifies the user has a valid session before your function runs and injects `context.user` + `context.session`:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';

export const myFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(MySchema)
  .handler(async ({ data, context }) => {
    const { user, session } = context; // authenticated user guaranteed

    // Function logic
  });
```

Under the hood `authMiddleware` calls `getSession()`, throws `Unauthorized` if anonymous, and injects `user` + `session` into `context`.

### organizationFunctionMiddleware

Use when your function requires organization context. Injects `context.organizationId` and `context.role` on top of the authenticated user:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { organizationFunctionMiddleware } from '@kit/function-middleware/functions';

export const createProjectFunction = createServerFn({ method: 'POST' })
  .middleware(organizationFunctionMiddleware)
  .validator(createProjectSchema)
  .handler(async ({ data, context }) => {
    const { user, organizationId, role } = context;
    // organizationId and role are guaranteed to exist
    // User is verified as a member of the active organization

    return db.insert(project).values({
      ...data,
      organizationId,
    }).returning();
  });
```

`organizationMiddleware` reads the active organization via `getActiveOrganizationId()`, throws `No active organization` if absent, and resolves the caller's role with `auth.api.getActiveMemberRole(...)`.

### adminFunctionMiddleware

For super-admin operations like banning users. Adds an admin role check on top of authentication and injects `context.user`:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { adminFunctionMiddleware } from '@kit/function-middleware/functions';

export const myAdminFunction = createServerFn({ method: 'POST' })
  .middleware(adminFunctionMiddleware)
  .validator(MySchema)
  .handler(async ({ data, context }) => {
    const { user } = context; // verified admin user

    // Admin-only logic
  });
```

`adminMiddleware` checks `context.user.role` against `ADMIN_ROLES` and throws `Forbidden` otherwise.

### Admin functions with RBAC

Spread `adminFunctionMiddleware` and append `withAdminPermission` to enforce admin RBAC requirements:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const banUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['ban'] })])
  .validator(banUserSchema)
  .handler(async ({ data, context }) => {
    // Only admins whose role grants user:ban reach here
  });
```

## Authorization Middleware

For granular permission checks, layer these middleware functions into the
`.middleware([...])` tuple. They each chain `authMiddleware` first, so they work on
top of any of the tuples above.

### withMinRole

Checks the user has at least the specified role level in the active (or supplied) organization. Role hierarchy is resolved via `getRoleHierarchy`.

```typescript
import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { withMinRole } from '@kit/function-middleware/server';

// Only admins and owners can reach this function
export const updateOrgSettingsFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withMinRole('admin')])
  .validator(updateOrgSettingsSchema)
  .handler(async ({ data, context }) => {
    // ctx.role is at least 'admin'; ctx.organizationId is available
  });

// Only owners can delete the organization
export const deleteOrgFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withMinRole('owner')])
  .validator(deleteOrgSchema)
  .handler(async ({ data, context }) => {
    // Only owners reach here
  });
```

By default `withMinRole` resolves the active organization. Pass
`{ getOrganizationId }` to resolve a specific organization from the user instead.

### withFeaturePermission

Checks the user's role has specific feature permissions (Better Auth `hasPermission`). Injects `context.organizationId`.

```typescript
import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { withFeaturePermission } from '@kit/function-middleware/server';

// Check a single permission
export const createFeedbackFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withFeaturePermission({ feedback: ['create'] })])
  .validator(createFeedbackSchema)
  .handler(async ({ data, context }) => {
    // Only roles with feedback:create reach here
  });

// Check multiple permissions
export const manageBoardFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withFeaturePermission({ board: ['update', 'delete'] })])
  .validator(manageBoardSchema)
  .handler(async ({ data, context }) => {
    // Requires both board:update AND board:delete
  });
```

### withAdminPermission

For admin functions, checks permissions defined in the admin RBAC configuration
(`getRolePermissions`). Spread `adminFunctionMiddleware` and append it:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const banUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['ban'] })])
  .validator(banUserSchema)
  .handler(async ({ data, context }) => {
    // Only admins whose role grants user:ban; context.permission is set
  });

// Multiple admin permissions
export const manageUserFunction = createServerFn({ method: 'POST' })
  .middleware([
    ...adminFunctionMiddleware,
    withAdminPermission({ user: ['get', 'ban'], session: ['revoke'] }),
  ])
  .validator(manageUserSchema)
  .handler(async ({ data, context }) => {
    // Requires all specified permissions
  });
```

## Usage Examples

### Basic Authenticated Function

Most functions follow this pattern: validate input with a Zod schema and perform the operation:

```typescript
import { z } from 'zod';
import { eq } from 'drizzle-orm';

import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { db, user } from '@kit/database';

const updateProfileSchema = z.object({
  name: z.string().min(1),
});

export const updateProfileFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(updateProfileSchema)
  .handler(async ({ data, context }) => {
    const [updated] = await db
      .update(user)
      .set({ name: data.name })
      .where(eq(user.id, context.user.id))
      .returning();

    return { success: true, user: updated };
  });
```

### Organization-Scoped Function

For multi-tenant operations, ensure all database queries include the organization filter:

```typescript
import { z } from 'zod';
import { and, eq } from 'drizzle-orm';

import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { withFeaturePermission } from '@kit/function-middleware/server';
import { db, project } from '@kit/database';

export const deleteProjectFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withFeaturePermission({ project: ['delete'] })])
  .validator(z.object({ projectId: z.string() }))
  .handler(async ({ data, context }) => {
    // withFeaturePermission injected context.organizationId
    await db
      .delete(project)
      .where(
        and(
          eq(project.id, data.projectId),
          eq(project.organizationId, context.organizationId),
        ),
      );
  });
```

### Admin Function

Admin functions operate at the platform level and delegate domain logic to services:

```typescript
import { createServerFn } from '@tanstack/react-start';

import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

import { revokeSessionSchema } from '../schemas';
import { createUserAdminService } from '../services/user-admin.service';

const service = createUserAdminService();

export const revokeSessionFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ session: ['revoke'] })])
  .validator(revokeSessionSchema)
  .handler(async ({ data, context }) => {
    return service.revokeSession({
      adminId: context.user.id,
      ...data,
    });
  });
```

## Error Handling

`errorMiddleware` (included in every middleware tuple) normalizes thrown errors through
`handleAuthError`: safe Better Auth error codes are preserved, everything else is
masked. `redirect()` / `notFound()` pass through. On the client, call functions with
TanStack Query and read the lifecycle from the mutation:

```typescript
'use client';

import { useMutation } from '@tanstack/react-query';
import { myFunction } from './my.functions';

function MyComponent() {
  const mutation = useMutation({ mutationFn: myFunction });

  const handleSubmit = (data: MyInput) => {
    mutation.mutate(
      { data },
      {
        onError: (error) => {
          // Auth/permission/validation errors surface here
          console.error(error);
        },
      },
    );
  };
}
```

## Context Properties

Middleware enriches the `context` object passed to your handler. The exact properties depend on which middleware ran:

| Middleware / Tuple | Context Properties |
|--------|-------------------|
| `authFunctionMiddleware` | `user`, `session` |
| `organizationFunctionMiddleware` | `user`, `session`, `organizationId`, `role` |
| `adminFunctionMiddleware` | `user`, `session` |
| `withMinRole` | adds `organizationId`, `role` |
| `withFeaturePermission` | adds `organizationId` |
| `withAdminPermission` | adds `permission` (`{ requirements, granted }`) |

## Creating Custom Middleware

Build custom middleware with `createMiddleware({ type: 'function' })` from
`@tanstack/react-start` when you need to add context or perform checks not covered
by the built-in options. Chain `authMiddleware` first if you need the user:

```typescript
import { createMiddleware, createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { authMiddleware } from '@kit/function-middleware/server';

// Custom middleware that loads the user's subscription
const withSubscription = createMiddleware({ type: 'function' })
  .middleware([authMiddleware])
  .server(async ({ next, context }) => {
    const [row] = await db
      .select()
      .from(subscription)
      .where(eq(subscription.userId, context.user.id))
      .limit(1);

    return next({ context: { subscription: row ?? null } });
  });

// Use the custom middleware
export const premiumFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withSubscription])
  .validator(schema)
  .handler(async ({ data, context }) => {
    if (!context.subscription || context.subscription.status !== 'active') {
      throw new Error('Active subscription required');
    }

    // Premium feature logic
  });
```

## Middleware Chain Order

When you layer multiple middleware, they execute in order and each can access the context produced by the previous one:

```typescript
export const complexFunction = createServerFn({ method: 'POST' })
  .middleware([
    ...authFunctionMiddleware,              // auth + errorMiddleware
    withMinRole('admin'),                 // 1. checks role
    withFeaturePermission({ project: ['update'] }), // 2. checks permissions
    withSubscription,                     // 3. loads subscription
  ])
  .validator(schema)
  .handler(async ({ data, context }) => {
    // context has: user, session, organizationId, role, subscription
  });
```

This middleware system is part of the [TanStack Start Drizzle SaaS Kit](/drizzle).

---

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