Function Middleware

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

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:

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:

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:

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:

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.

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.

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:

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:

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:

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:

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:

'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 / TupleContext Properties
authFunctionMiddlewareuser, session
organizationFunctionMiddlewareuser, session, organizationId, role
adminFunctionMiddlewareuser, session
withMinRoleadds organizationId, role
withFeaturePermissionadds organizationId
withAdminPermissionadds 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:

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:

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.


Next: Working with Forms →