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 functionexport 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 organizationexport 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 permissionexport 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 permissionsexport 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 permissionsexport 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 / 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:
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 subscriptionconst 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 middlewareexport 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 →