Server
For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Protected calls

Validate server function inputs, resolve the caller from its session, enforce record ownership, and guard raw HTTP handlers on the Worker.

Authorization belongs next to the data access. Require a session, validate the input, and constrain the database operation to that session's user. None of those checks can be replaced by a hidden button or a route redirect.

Record ownership

This example expects the new notes table from schema changes. The schema stays client-safe:

src/modules/notes/notes.schema.ts
import * as z from "zod";

export const noteIdSchema = z.object({ id: z.string().uuid() });

The server helper checks both the record ID and its owner:

src/modules/notes/notes.server.ts
import { and, eq } from "drizzle-orm";

import { db } from "@/db";
import { note } from "@/db/schema";

export async function findNote(userId: string, id: string) {
  const [record] = await db
    .select()
    .from(note)
    .where(and(eq(note.id, id), eq(note.userId, userId)));
  return record ?? null;
}

Wire the helper through a protected function:

src/modules/notes/notes.functions.ts
import { createServerFn } from "@tanstack/react-start";

import { enforceAuth } from "@/lib/auth/middleware";

import { noteIdSchema } from "./notes.schema";
import { findNote } from "./notes.server";

export const getNote = createServerFn({ method: "GET" })
  .middleware([enforceAuth])
  .validator(noteIdSchema)
  .handler(({ data, context }) => findNote(context.user.id, data.id));

The client calls getNote({ data: { id } }). It cannot choose another owner because no userId is accepted as input. Apply the same owner predicate to updates and deletes, not only reads.

The snippets use .validator, matching the installed kit's function wrappers. When upgrading TanStack Start, check the installed guidance for changes to its input validation API.

HTTP endpoints

For an endpoint that returns raw HTTP, attach the shared request middleware under server:

import { createFileRoute } from "@tanstack/react-router";

import { enforceAuth } from "@/lib/auth/middleware";

export const Route = createFileRoute("/api/current-user")({
  server: {
    middleware: [enforceAuth],
    handlers: {
      GET: ({ context }) => Response.json({ id: context.user.id }),
    },
  },
});

This is a new example route, not a shipped endpoint. Create its file and let route generation register it. The existing storage and chat routes use this middleware pattern.

Access policies

enforceAuth accepts anonymous users with valid sessions. If your action requires a registered or verified account, check context.user.isAnonymous and context.user.emailVerified on the server. Use plan middleware for paid access. Captcha proves a bot challenge was checked, not who owns a record.

Return only fields the caller needs. Treat serializable output as browser-visible even when it comes from a .server.ts helper.

How is this guide?

Last updated on

On this page

Ship globally on the edge. In minutes.Try Edge Kit