# Adding Features

> Step-by-step guide to adding new features to your TanStack Start Prisma SaaS application with multi-tenancy, type safety, and authorization.

*Canonical: https://makerkit.dev/docs/tanstack-prisma/development-guide/adding-features*

---

Building features in a multi-tenant SaaS application requires careful attention to data isolation, authorization, and type safety. This guide walks through a complete example (a "Projects" feature) demonstrating how to structure your code from database schema through to the user interface.

By the end of this guide, you'll have built a complete CRUD feature with proper organization scoping, server functions with validation, and a polished UI.

## Feature Planning

Before writing code, answer these questions to clarify scope and prevent rework:

1. **Define requirements** — What does this feature do? What problem does it solve?
2. **Design data model** — What data needs to be stored? How does it relate to existing entities?
3. **Identify user functions** — What can users create, read, update, or delete?
4. **Determine access control** — Who can access this? Team members only? Specific roles?

This is useful for both manual development and when using AI to help you with the feature - as it will help define the scope of what you need to build.

## Step-by-Step Example

The following example builds a "Projects" feature where organization members can create and manage projects. Each step builds on the previous one, creating a type-safe chain from validation through to the UI.

We will cover everything you need to know to add a new feature to your application:
- Defining the Zod validation schema
- Defining the database schema and creating a new migration
- Creating the server functions
- Fetching data in route loaders
- Building the UI components
- Creating the route
- Verifying the feature

### Project Structure

TanStack Start uses file-based routing. Routes live under `apps/web/src/routes`,
shared UI under `apps/web/src/components`, and server/loader/schema code under
`apps/web/src/lib`. A typical layout for the projects feature:

```text
apps/web/src/routes/_authenticated/projects/
├── index.tsx          # list route: loader + component
└── $projectId.tsx     # detail route: dynamic segment

apps/web/src/components/projects/
└── project-form.tsx

apps/web/src/lib/projects/
├── project.schema.ts
├── projects.loader.ts
└── projects.functions.ts
```

The pathless `_authenticated` group (`apps/web/src/routes/_authenticated/route.tsx`)
already guards auth and renders the app shell, so every nested route inherits the
sidebar and the authenticated context.

### Step 1: Define Schemas

Start with the [Zod](https://zod.dev) validation schema.

This schema serves double duty: **it validates form input on the client and server-function input on the server**.

Defining it first ensures type safety flows through your entire feature.

**💡 Tip:** If you use Linux or MacOS, use the command `touch` to quickly create a new file in your terminal.
```bash
touch apps/web/src/lib/projects/project.schema.ts
```

On Windows, you can use the command `New-Item` to create a new file in your terminal.
```
New-Item -Path apps/web/src/lib/projects/project.schema.ts -ItemType File
```

Better yet, on VSCode (or Cursor), you can use the command palette to create a new file.

This will create a new file called `project.schema.ts` in the `lib/projects` directory.

You can then open the file and add the following code:

```typescript {% title="apps/web/src/lib/projects/project.schema.ts" %}
import * as z from 'zod';

export const createProjectSchema = z.object({
  name: z.string().min(3, 'Name must be at least 3 characters'),
});

export const updateProjectSchema = createProjectSchema.partial().extend({
  id: z.uuid(),
});

export type CreateProjectInput = z.output<typeof createProjectSchema>;
export type UpdateProjectInput = z.output<typeof updateProjectSchema>;
```

### Step 2: Define Database Schema

With the validation schema in place, we move on to defining the database table.

The `organizationId` foreign key establishes the multi-tenant relationship — every project belongs to exactly one organization.

In the example below, we use Postgres and Prisma ORM to define the `projects` table by editing the shared Prisma schema.

Add the model to `packages/database/src/prisma/schema.prisma`:

```prisma {% title="packages/database/src/prisma/schema.prisma" %}
model Project {
  id             String       @id @default(uuid())
  name           String
  organizationId String       @map("organization_id")
  createdAt      DateTime     @default(now()) @map("created_at")
  updatedAt      DateTime     @updatedAt @map("updated_at")

  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)

  @@map("projects")
}
```

Don't forget to add the back-relation to the `Organization` model:

```prisma
model Organization {
  // ... existing fields
  projects Project[]
}
```

**Understanding the schema:**

- **`model Project`** — Defines a Prisma model that maps to a PostgreSQL table named `projects`
- **`@id @default(uuid())`** — A text-based primary key with automatic UUID generation. You can also generate the ID in code using `generateId()` (UUIDv7) from `@kit/shared/uuid` when inserting records.
- **`String`** — A required text field. Use `String?` for optional fields
- **`organizationId`** — The foreign key that links each project to an organization. The `@relation` creates a database-level constraint, and `onDelete: Cascade` ensures projects are automatically deleted when their parent organization is removed
- **`@updatedAt`** — Prisma automatically updates this timestamp on every update
- **`@@map("projects")`** — Maps the model name to a specific table name in the database

> **💡 Tip**: For more information about using Prisma ORM, please refer to the [Prisma ORM documentation](https://www.prisma.io/docs).

After defining the schema, regenerate the Prisma client by running the following command:

```bash
pnpm --filter "@kit/database" prisma:generate
```

The command above regenerates the Prisma client from the shared schema. The output will be something like this:

```
Environment variables loaded from .env
Loaded Prisma config from prisma.config.ts
Prisma schema loaded from src/prisma/schema.prisma
Datasource "db": PostgreSQL database

✔ Generated Prisma Client (v7.x.x) to ./src/prisma/generated
```

Now create and apply a migration:

```bash
pnpm --filter "@kit/database" prisma:migrate
```

Prisma will prompt you for a migration name. Enter something like `add_projects`. The output will look like:

```
Environment variables loaded from .env
Loaded Prisma config from prisma.config.ts
Prisma schema loaded from src/prisma/schema.prisma

✔ Enter a name for the new migration: add_projects
Applying migration `20241226_add_projects`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20241226_add_projects/
    └─ migration.sql

✔ Generated Prisma Client (v7.x.x) to ./src/prisma/generated
```

You can then open the migration file in `packages/database/src/prisma/migrations/` and review the changes. Please note that the migration directory name will look different from the above one, so please check the output of the command above to locate it. For more information, see the [Prisma Migrate documentation](https://www.prisma.io/docs/orm/prisma-migrate).

Let's now navigate to the Prisma Studio to explore our new table.

Run the following command to start Prisma Studio:

```bash
pnpm --filter "@kit/database" prisma:studio
```

If everything is working correctly, the CLI will list the URL at which you can access Prisma Studio:

> Prisma Studio is up and running on http://localhost:5555

Open the URL in your browser to explore the new table. You should see the new `projects` table. It's a good idea to familiarize yourself with Prisma Studio, as it will become an invaluable tool for you when working with the database.

{% img src="/images/docs/prisma-studio.webp" width="2856" height="1972" alt="Prisma Studio database interface" /%}

### Step 3: Create Server Functions

Server functions handle mutations. In the example below, we create three: `createProjectFunction`, `updateProjectFunction`, and `deleteProjectFunction`.

TanStack Start compiles these into POST endpoints automatically — but we call them like normal async JavaScript functions (through TanStack Query). Pretty neat, right?

We start each one from `createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)`, which guarantees the user is
authenticated, that there is an active organization, and injects
`context.organizationId` and `context.role`.

💡 **Note:** The server-function middleware tuples (`authFunctionMiddleware`, `organizationFunctionMiddleware`, `adminFunctionMiddleware`, `withAdminPermission`) live in the `@kit/function-middleware` package. Please always start from one of these when creating server functions!

Notice how the Zod schema from Step 1 validates input automatically via `.validator()`:

```typescript {% title="apps/web/src/lib/projects/projects.functions.ts" %}
import * as z from 'zod';

import { createServerFn } from '@tanstack/react-start';
import { organizationFunctionMiddleware } from '@kit/function-middleware/functions';
import { db } from '@kit/database';
import { generateId } from '@kit/shared/uuid';

import { createProjectSchema, updateProjectSchema } from './project.schema';

export const createProjectFunction = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)
  .validator(createProjectSchema)
  .handler(async ({ data, context }) => {
    const newProject = await db.project.create({
      data: {
        ...data,
        id: generateId(),
        organizationId: context.organizationId,
      },
    });

    return newProject;
  });

export const updateProjectFunction = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)
  .validator(updateProjectSchema)
  .handler(async ({ data, context }) => {
    const updatedProject = await db.project.update({
      where: {
        id: data.id,
        organizationId: context.organizationId,
      },
      data,
    });

    return updatedProject;
  });

export const deleteProjectFunction = createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)
  .validator(z.object({ id: z.string() }))
  .handler(async ({ data, context }) => {
    await db.project.delete({
      where: {
        id: data.id,
        organizationId: context.organizationId,
      },
    });
  });
```

**Understanding the server functions:**
- **`createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware)`** — Pre-binds the auth + organization middleware. `context.user`, `context.organizationId`, and `context.role` are guaranteed.
- **`.validator(schema)`** — The Zod schema that validates the input of the function
- **`.handler(...)`** — The server-side implementation. Its `{ data, context }` argument is fully typed.
- **`generateId`** — Generates a new ID for the project using UUIDv7, a performant and secure way to generate a unique ID. Import from `@kit/shared/uuid`.

> **Refreshing data:** there is no `revalidatePath` in TanStack Start. After a
> mutation, refresh server data by invalidating the router from the client —
> `router.invalidate()` (see Step 5) re-runs the affected route loaders.

### Step 4: Create Data Loader

Route loaders fetch data on the server before the component renders. Because the
database client is server-only, we expose the queries through
`createServerFn({ method: 'GET' })` so the loader can call them during both initial
SSR and client-side navigation.

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

import { requireActiveOrganizationId } from '@kit/better-auth/context';
import { db } from '@kit/database';

export const getProjects = createServerFn({ method: 'GET' }).handler(async () => {
  const organizationId = await requireActiveOrganizationId();

  return db.project.findMany({
    where: { organizationId },
    orderBy: { createdAt: 'desc' },
  });
});

export const getProject = createServerFn({ method: 'GET' })
  .validator((id: string) => id)
  .handler(async ({ data: id }) => {
    const organizationId = await requireActiveOrganizationId();

    const result = await db.project.findFirst({
      where: { id, organizationId },
    });

    return result ?? null;
  });
```

**Understanding the loader:**

- **`createServerFn({ method: 'GET' })`** — Marks this as a server-only read. It can be called from route loaders and runs on the server; the database client is never bundled into client code.
- **`requireActiveOrganizationId()`** — Reads the active organization from the request context and throws if none is set. Every tenant-scoped query is filtered by it.
- **`db.project.findMany(...)`** — Prisma's query API. It's intuitive and provides full type safety.
- **`where` and `orderBy`** — Prisma options for filtering and sorting results. They compile to `WHERE` and `ORDER BY` clauses.

The `getProject` function demonstrates a common pattern for fetching a single record: `findFirst()` with a tenant-scoped `where` returns the record or `null` when nothing matches.

### Step 5: Build UI Components

The form component ties everything together on the client. It uses `useForm` from `@tanstack/react-form` for form state, the Zod schema for validation via `validators`, and TanStack Query's `useMutation` to call the server function with automatic lifecycle handling.

We will wrap the form in a `Sheet` component to open a side panel to create a new project.

**Tip**: it's a good practice to use separate components for the form, and for the container that wraps the form (be this a `Dialog`, `AlertDialog`, `Sheet`, etc.). This will make your code more readable and maintainable, and will instantiate the form component only when needed (e.g., when the user clicks the "Create Project" button in the header).

```typescript {% title="apps/web/src/components/projects/project-form.tsx" %}
'use client';

import { useState } from 'react';

import { useForm } from '@tanstack/react-form';
import { useMutation } from '@tanstack/react-query';
import { useRouter } from '@tanstack/react-router';

import { Button } from '@kit/ui/button';
import { Field, FieldError, FieldLabel } from '@kit/ui/field';
import { Input } from '@kit/ui/input';
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from '@kit/ui/sheet';
import { toast } from '@kit/ui/sonner';

import { createProjectFunction } from '#/lib/projects/projects.functions';
import { createProjectSchema } from '#/lib/projects/project.schema';

export function ProjectFormContainer({ children }: React.PropsWithChildren) {
  const [open, setOpen] = useState(false);

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={children} />

      <SheetContent className="flex flex-col gap-y-4">
        <SheetHeader className="gap-y-0">
          <SheetTitle>Create Project</SheetTitle>

          <SheetDescription className="text-base">
            Create a new project to get started.
          </SheetDescription>
        </SheetHeader>

        <ProjectForm onSuccess={() => setOpen(false)} />
      </SheetContent>
    </Sheet>
  );
}

function ProjectForm({ onSuccess }: { onSuccess: () => void }) {
  const router = useRouter();
  const createProject = useMutation({ mutationFn: createProjectFunction });

  const form = useForm({
    defaultValues: {
      name: '',
    },
    validators: {
      onChange: createProjectSchema,
      onSubmit: createProjectSchema,
    },
    onSubmit: ({ value }) => {
      toast.promise(
        createProject.mutateAsync({ data: value }).then(async () => {
          await router.invalidate(); // re-run the projects loader
          onSuccess();
        }),
        {
          loading: 'Creating project...',
          success: 'Project created!',
          error: 'Failed to create project',
        },
      );
    },
  });

  const isPending = createProject.isPending;

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      className="space-y-4"
    >
      <form.Field name="name">
        {(field) => {
          const isInvalid =
            field.state.meta.isTouched && !field.state.meta.isValid;

          return (
            <Field data-invalid={isInvalid}>
              <FieldLabel>Project Name</FieldLabel>

              <Input
                name={field.name}
                disabled={isPending}
                placeholder="e.g. My Project"
                value={field.state.value}
                onBlur={field.handleBlur}
                onChange={(e) => field.handleChange(e.target.value)}
                aria-invalid={isInvalid}
              />

              <FieldError errors={field.state.meta.errors} />
            </Field>
          );
        }}
      </form.Field>

      <Button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Project'}
      </Button>
    </form>
  );
}
```

We will be using this component in the route later on.

**Note**: since we're using React Hooks and interactive components, we add the `use client` directive. This **does not mean that the component only renders on the client side** — it still server-renders during SSR and then hydrates on the client.

### Step 6: Create the Route

The list route fetches data in its `loader` and renders the UI in its `component`. The component reads the loaded data with `Route.useLoaderData()`:

```tsx {% title="apps/web/src/routes/_authenticated/projects/index.tsx" %}
import { Link, createFileRoute, redirect } from '@tanstack/react-router';

import { getActiveOrganization } from '@kit/better-auth/context';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Button } from '@kit/ui/button';
import {
  CardButton,
  CardButtonHeader,
  CardButtonTitle,
} from '@kit/ui/card-button';
import {
  EmptyState,
  EmptyStateButton,
  EmptyStateHeading,
  EmptyStateText,
} from '@kit/ui/empty-state';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';

import { ProjectFormContainer } from '#/components/projects/project-form';
import { getProjects } from '#/lib/projects/projects.loader';

export const Route = createFileRoute('/_authenticated/projects/')({
  loader: async () => {
    const organization = await getActiveOrganization();

    if (!organization) {
      throw redirect({ to: '/dashboard' });
    }

    const projects = await getProjects();

    return { organization, projects };
  },
  component: ProjectsPage,
});

function ProjectsPage() {
  const { organization, projects } = Route.useLoaderData();

  return (
    <PageBody>
      <PageHeader>
        <div className="flex-1">
          <AppBreadcrumbs
            values={{
              dashboard: organization.name ?? 'Dashboard',
            }}
          />
        </div>

        <div className="flex justify-end">
          <ProjectFormContainer>
            <Button size="sm">Create Project</Button>
          </ProjectFormContainer>
        </div>
      </PageHeader>

      <If condition={projects.length === 0}>
        <EmptyState>
          <EmptyStateHeading>No projects found</EmptyStateHeading>
          <EmptyStateText>Create a new project to get started.</EmptyStateText>

          <ProjectFormContainer>
            <EmptyStateButton>Create Project</EmptyStateButton>
          </ProjectFormContainer>
        </EmptyState>
      </If>

      <If condition={projects.length > 0}>
        <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
          {projects.map((project) => (
            <Link
              key={project.id}
              to="/projects/$projectId"
              params={{ projectId: project.id }}
            >
              <CardButton>
                <CardButtonHeader>
                  <CardButtonTitle className="text-left">
                    <span className="truncate">{project.name}</span>
                  </CardButtonTitle>
                </CardButtonHeader>
              </CardButton>
            </Link>
          ))}
        </div>
      </If>
    </PageBody>
  );
}
```

**What's happening here:**

This route brings together everything we built in the previous steps. Let's break it down:

1. **Authorization check** — The `loader` calls `getActiveOrganization()`. If there isn't one, we `throw redirect({ to: '/dashboard' })`. Loaders run on the server during SSR and during client navigation, so this guard runs before the component renders.
2. **Data fetching** — We call our `getProjects()` server function from the loader. The active organization is resolved server-side from the request context, so the query is always tenant-scoped.
3. **Reading data** — The component calls `Route.useLoaderData()` to read what the loader returned, fully typed.
4. **Page structure** — We use `PageBody` and `PageHeader` for a consistent layout, with breadcrumbs and a "Create Project" button wrapped in our `ProjectFormContainer` (the sheet we built earlier).
5. **Conditional rendering** — The `If` component handles two states: an empty state when there are no projects, and a grid of project cards when projects exist.
6. **Project cards** — Each project renders as a `CardButton` inside a typed `Link` (`to` + `params`). The `key={project.id}` prop helps React efficiently update the list.

The interactive parts are the `ProjectFormContainer` components, which are Client Components that handle the sheet behavior.

#### About layouts

You usually don't need a per-feature layout. The pathless `_authenticated` group
(`apps/web/src/routes/_authenticated/route.tsx`) already renders the app shell —
sidebar, header, and the authenticated context — for every nested route. If a
specific feature needs a different shell, add a nested pathless layout route
(`apps/web/src/routes/_authenticated/projects/route.tsx`) that renders an
`<Outlet />` inside your custom layout.

### Step 7: Adding the route to the sidebar

To add the route to the sidebar, add it to the `routes` array in the navigation config.

```tsx {% title="apps/web/src/config/navigation.config.tsx" %}
import { Folder, Home } from 'lucide-react';
import * as z from 'zod';

import { env } from '@kit/shared/env';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';

const iconClasses = 'w-4';

/**
 * Navigation routes for the authenticated app section.
 * Items adapt based on the current account context via getContextAwareNavigation.
 */
export const routes: z.output<typeof NavigationConfigSchema>['routes'] = [
  {
    label: 'common.routes.application',
    children: [
      {
        label: 'common.routes.dashboard',
        path: env('VITE_APP_HOME_PATH') ?? '/dashboard',
        Icon: <Home className={iconClasses} />,
      },
      {
        label: 'Projects',
        path: '/projects',
        Icon: <Folder className={iconClasses} />,
        context: 'organization',
      },
    ],
  },
];
```

**Note**: for simplicity, we are not using a `common.routes.projects` translation key. You can translate the label by adding a key to `packages/i18n/src/messages/en/common.json` and using it as the `label`.

**Understanding the context:**

- **`context: 'organization'`** — Scopes the nav item to the organization context. If you omit the `context` property, the item is shown in both personal and organization contexts.

### Step 8: Let's see the result

Below is a screenshot of the projects page:

{% img src="/images/docs/projects-page-empty.webp" width="2856" height="1972" alt="Empty projects page state" /%}

You can now create a new project by clicking the "Create Project" button in the header.

{% img src="/images/docs/create-project-sheet.webp" width="2856" height="1972" alt="Create project slide-over form" /%}

As you can see, as we don't currently have any projects, the empty state is displayed. Let's create a new project by clicking the "Create Project" button in the sheet:

{% img src="/images/docs/projects-page.webp" width="2856" height="1972" alt="Projects list page with data" /%}

### Step 9: Create the project detail route

At the moment, when you click on a project card, you are routed to a 404. This is totally normal and expected, as we haven't created the project detail route yet.

Let's create a detail route at `apps/web/src/routes/_authenticated/projects/$projectId.tsx`. The `$projectId` filename creates a dynamic segment that we read from `params`:

```tsx {% title="apps/web/src/routes/_authenticated/projects/$projectId.tsx" %}
import { createFileRoute, notFound } from '@tanstack/react-router';

import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { PageBody, PageHeader } from '@kit/ui/page';

import { getProject } from '#/lib/projects/projects.loader';

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

    if (!project) {
      throw notFound();
    }

    return { project };
  },
  component: ProjectDetailPage,
});

function ProjectDetailPage() {
  const { project } = Route.useLoaderData();

  return (
    <PageBody>
      <PageHeader>
        <AppBreadcrumbs values={{ [project.id]: project.name }} />
      </PageHeader>

      <div className="border-border rounded-lg border p-4">
        <h1 className="text-2xl font-bold">{project.name}</h1>
      </div>
    </PageBody>
  );
}
```

**What's happening here:**

1. **Dynamic route** — The `$projectId` filename creates a dynamic segment. When a user visits `/projects/abc123`, TanStack Router exposes `abc123` as `params.projectId`.
2. **Reading params in the loader** — The loader receives `{ params }` and passes the id to our server function: `getProject({ data: params.projectId })`.
3. **Fetching the project** — We reuse the `getProject()` server function from Step 4. It is tenant-scoped, so a project from another organization can't be loaded.
4. **Handling missing projects** — If `getProject()` returns `null`, we `throw notFound()`. This renders the router's not-found UI and stops execution.
5. **Breadcrumbs with dynamic values** — `AppBreadcrumbs` generates breadcrumbs from the URL path. The `values` prop overrides specific segments with human-readable names:

```tsx
<AppBreadcrumbs values={{ [project.id]: project.name }} />
```

This maps the project ID in the URL to the project's actual name. So instead of `Home / Projects / abc123`, users see `Home / Projects / My Project`.

{% img src="/images/docs/project-detail-page.webp" width="2856" height="1972" alt="Project detail page view" /%}

### Step 10: Verify

Before committing, run these checks to ensure everything works correctly:

```bash
# Check types — catches schema mismatches and missing imports
pnpm typecheck

# Fix lint issues — auto-fixes formatting and style problems
pnpm lint:fix

# Format code
pnpm format:fix
```

## Summary

You've now built a complete feature following the layered architecture:

| Layer | Files Created |
|-------|--------------|
| **Validation** | `src/lib/projects/project.schema.ts` (Zod schemas) |
| **Database** | `packages/database/src/prisma/schema.prisma` (Project model) |
| **Server Functions** | `src/lib/projects/projects.functions.ts` |
| **Data Loader** | `src/lib/projects/projects.loader.ts` |
| **UI Components** | `src/components/projects/project-form.tsx` |
| **Routes** | `src/routes/_authenticated/projects/index.tsx`, `.../$projectId.tsx` |

This pattern scales well. As you add more features, the consistent structure makes your codebase navigable and maintainable.

{% faq
   title="Common Questions"
   items=[
     {"question": "Do I need to create a package for every feature?", "answer": "No. Place app features under apps/web/src/routes, components, and lib by default. Only create a workspace package under packages/ when you need to share code across multiple apps or when the feature becomes complex enough to warrant isolation."},
     {"question": "Should I use createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware) or createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware)?", "answer": "Use createServerFn({ method: 'POST' }).middleware(organizationFunctionMiddleware) for organization-scoped mutations: it guarantees an active organization and injects context.organizationId and context.role. Use createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware) (with requireActiveOrganizationId() when needed) for functions that only require an authenticated user."},
     {"question": "How do I add authorization to specific functions?", "answer": "Layer withFeaturePermission or withMinRole via the native .middleware([...]) step. For example: createServerFn({ method: 'POST' }).middleware(authFunctionMiddleware).middleware([withFeaturePermission({ project: ['delete'] })]).validator(schema).handler(...)"},
     {"question": "How do I refresh the list after a mutation?", "answer": "There is no revalidatePath in TanStack Start. Call router.invalidate() (from useRouter()) in the mutation's success callback to re-run the affected route loaders, or invalidate a TanStack Query cache with queryClient.invalidateQueries(...)."},
     {"question": "What if I need to update multiple tables atomically?", "answer": "Wrap your operations in db.transaction(). If any operation fails, all changes roll back automatically. See the Database Operations guide for transaction examples."}
   ]
/%}

---

**Next:** [Server Functions →](./server-functions)
