# Server Functions

> Build type-safe server functions for handling user mutations with authentication and validation in your TanStack Start Drizzle SaaS application.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/development-guide/server-functions*

---

Server functions are the backbone of data mutations in your application. They run on the server, have full access to your database, and can be called directly from client components without hand-writing API endpoints.

> **Note:** In TanStack Start, mutations are **server functions** created with `createServerFn`. This repo attaches authorization to a literal `createServerFn(...)` call via the pre-composed middleware tuples in `@kit/function-middleware` (`authFunctionMiddleware`, `organizationFunctionMiddleware`, `adminFunctionMiddleware`), optionally spread with gates like `withAdminPermission`.

## What are Server Functions?

Server functions are async functions that execute on the server when invoked from the client. They're ideal for form submissions, data mutations, and any operation requiring server-side logic. Key benefits include:

- **Type safety** — Full TypeScript support from client to server, including the validated input and the middleware-injected `context`
- **Automatic serialization** — Arguments and return values are serialized automatically
- **Built-in error handling** — `errorMiddleware` normalizes thrown errors; router `redirect()` / `notFound()` pass through
- **Session management** — Access the authenticated user, organization, and role through middleware-injected `context`

## Basic Server Function

The lowest-level primitive is `createServerFn` from `@tanstack/react-start`. Mutations use `{ method: 'POST' }`:

```typescript {% title="apps/web/src/lib/example/my.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { getSession } from '@kit/better-auth/context';

export const myFunction = createServerFn({ method: 'POST' })
  .validator((data: { name: string }) => data)
  .handler(async ({ data }) => {
    const result = await getSession();

    if (!result) {
      throw new Error('Unauthorized');
    }

    // Perform operation
    return { success: true };
  });
```

In practice you rarely check auth by hand — you attach a middleware tuple from `@kit/function-middleware` that injects the authenticated `context` for you.

## With Authentication and Validation

Never trust client input. Pass a Zod schema to `.validator()` to validate data before the handler runs. The `authFunctionMiddleware` tuple handles authentication automatically, injecting `context.user` and `context.session`, so you can focus on business logic:

```typescript {% title="apps/web/src/lib/example/create-user.functions.ts" %}
import { z } from 'zod';

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

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

const CreateUserSchema = z.object({
  name: z.string().min(3),
  email: z.email(),
});

export const createUserFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(CreateUserSchema)
  .handler(async ({ data, context }) => {
    // context.user available from authMiddleware
    // data already validated by the schema
    const [newUser] = await db.insert(user).values(data).returning();

    return newUser;
  });
```

Attach the tuple to a literal `createServerFn(...)` call, then continue the chain
with `.validator(...)`, optionally more `.middleware([...])` gates, and `.handler(...)`.

## A Real Function

The kit's organization name update is a complete, real example. It delegates the
write to a service and reads the active organization from the request context:

```typescript {% title="packages/organization/ui/src/lib/server/functions/update-organization-name.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { requireActiveOrganizationId } from '@kit/better-auth/context';
import { UpdateOrganizationNameSchema } from '@kit/organization-core/schemas/update-organization-name.schema';
import { createUpdateOrganizationService } from '@kit/organization-core/services';

/**
 * Update the active organization's name using Better Auth
 */
export const updateOrganizationNameFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(UpdateOrganizationNameSchema)
  .handler(async ({ data }) => {
    const organizationId = await requireActiveOrganizationId();
    const service = createUpdateOrganizationService();

    await service.updateOrganization({
      organizationId,
      name: data.name,
    });
  });
```

## Error Handling

Every middleware tuple includes `errorMiddleware`, which routes thrown errors through
`handleAuthError` — safe Better Auth error codes are preserved, everything else is
masked. Router control-flow signals (`redirect()` / `notFound()`) pass through
untouched, so you generally just `throw` and let the middleware do the right thing:

```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 }) => {
    await someOperation(data);

    // Throwing here is normalized by errorMiddleware.
    // redirect()/notFound() (from @tanstack/react-router) pass through.
    return { success: true };
  });
```

Avoid wrapping the whole handler in a `try/catch` that swallows errors — that
defeats `errorMiddleware` and (in callers) breaks `redirect()` navigation.

## Redirecting

Redirect from a route `loader` or `beforeLoad` using `redirect()` from
`@tanstack/react-router`. For post-mutation navigation, prefer navigating on the
client from the mutation's success callback (see [Working with Forms](./working-with-forms)):

```typescript
import { redirect } from '@tanstack/react-router';

export const Route = createFileRoute('/_authenticated/projects/$id')({
  loader: async ({ params }) => {
    const project = await fetchProject({ data: params.id });

    if (!project) {
      throw redirect({ to: '/projects' });
    }

    return { project };
  },
});
```

## Organization-Scoped Functions

In a multi-tenant application, every operation must be scoped to the current organization. The `organizationFunctionMiddleware` tuple injects `context.organizationId` and `context.role` and guarantees the user is a member of the active organization:

```typescript
import { z } from 'zod';

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

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

export const listProjectsFunction = createServerFn({ method: 'POST' })
  .middleware(organizationFunctionMiddleware)
  .validator(z.object({}))
  .handler(async ({ context }) => {
    // context.organizationId and context.role available from middleware
    // User is verified as a member of the organization

    return db.query.project.findMany({
      where: (project, { eq }) => eq(project.organizationId, context.organizationId),
    });
  });
```

## Permission Checks

Authorization should be declarative. Layer extra middleware with the native
`.middleware([...])` step. Use `withFeaturePermission` for resource-based checks or
`withMinRole` for role-hierarchy checks:

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

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

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

const DeleteProjectSchema = z.object({ projectId: z.string() });

// Using a resource:action permission check
export const deleteProjectFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withFeaturePermission({ project: ['delete'] })])
  .validator(DeleteProjectSchema)
  .handler(async ({ data, context }) => {
    // Permission already verified by middleware; context.organizationId is set
    await db.delete(project).where(eq(project.id, data.projectId));
  });

// Or using a minimum role check
export const adminOnlyFunction = createServerFn({ method: 'POST' })
  .middleware([...authFunctionMiddleware, withMinRole('admin')])
  .validator(z.object({}))
  .handler(async ({ context }) => {
    // Only admins and owners reach here; context.role is guaranteed
  });
```

## Admin Functions

Super-admin operations use the `adminFunctionMiddleware` tuple (admin role required),
optionally spread with `withAdminPermission(requirements)` for an admin RBAC check:

```typescript {% title="packages/admin/src/use../lib/functions/user-admin.functions.ts" %}
import { createServerFn } from '@tanstack/react-start';

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

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

const service = createUserAdminService();

export const banUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['ban'] })])
  .validator(banUserSchema)
  .handler(async ({ data, context }) => {
    return service.banUser({
      adminId: context.user.id,
      ...data,
    });
  });
```

## Logging in Server Functions

Adding structured logging helps debug production issues. Use the logger service to track execution:

```typescript
import { z } from 'zod';

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

import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { getLogger } from '@kit/shared/logger';
import { generateId } from '@kit/shared/uuid';
import { db, project } from '@kit/database';

const CreateProjectSchema = z.object({ name: z.string() });

export const createProjectFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(CreateProjectSchema)
  .handler(async ({ data, context }) => {
    const logger = (await getLogger()).child({
      name: 'projects.create',
      userId: context.user.id,
    });

    logger.info('Creating project...');

    try {
      const [created] = await db
        .insert(project)
        .values({ id: generateId(), name: data.name })
        .returning();

      logger.info({ projectId: created.id }, 'Project created');

      return { project: created };
    } catch (error) {
      logger.error({ error }, 'Failed to create project');
      throw error;
    }
  });
```

## Best Practices

Follow these guidelines for robust server functions:

1. **Always validate input** — Pass a Zod schema to `.validator()` for type-safe validation
2. **Use the middleware tuples** — attach `authFunctionMiddleware` / `organizationFunctionMiddleware` / `adminFunctionMiddleware` to a literal `createServerFn(...)`; don't roll your own auth
3. **Verify permissions** — Layer `withFeaturePermission` or `withMinRole` via `.middleware([...])`
4. **Let errors flow** — `errorMiddleware` normalizes thrown errors and lets `redirect()` / `notFound()` pass through
5. **Scope to organizations** — Every query should include the organization context (`context.organizationId` or `requireActiveOrganizationId()`)
6. **Keep business logic in services** — Actions orchestrate; services own the domain logic
7. **Add logging** — Add structured logs for debugging production issues
8. **Refresh loader data on the client** — Server functions don't revalidate loaders (no `revalidatePath`). After a mutation, `await router.invalidate()` in the caller's success path — see [Revalidating After a Mutation](./working-with-forms#revalidating-after-a-mutation)

## Common Mistakes

### Swallowing errors with try/catch

```typescript
// ❌ Wrong - masks the error and breaks errorMiddleware/redirect handling
export const myFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(MySchema)
  .handler(async ({ data }) => {
    try {
      await doWork(data);
    } catch {
      return { success: false };
    }
  });

// ✅ Correct - throw and let errorMiddleware normalize it
export const myFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(MySchema)
  .handler(async ({ data }) => {
    await doWork(data);
    return { success: true };
  });
```

### Missing organization scope

```typescript
// ❌ Wrong - returns all projects across tenants
const projects = await db.select().from(project);

// ✅ Correct - scoped to the active organization
const organizationId = await requireActiveOrganizationId();
const projects = await db
  .select()
  .from(project)
  .where(eq(project.organizationId, organizationId));
```

### Re-fetching context the middleware already provides

```typescript
// ❌ Wrong - organizationFunctionMiddleware already injects context.organizationId
export const action = createServerFn({ method: 'POST' })
  .middleware(organizationFunctionMiddleware)
  .validator(schema)
  .handler(async () => {
    const organizationId = await getActiveOrganizationId();
  });

// ✅ Correct - read it from context
export const action = createServerFn({ method: 'POST' })
  .middleware(organizationFunctionMiddleware)
  .validator(schema)
  .handler(async ({ context }) => {
    const { organizationId, role } = context;
  });
```

---

**Next:** [Function Middleware →](./function-middleware)
