A full-stack app
An app with its own API, server actions and live updates, in one codebase.
When you own the backend too, keep it in the same project. The fullstack kit runs on TanStack Start: pages render on the server, server actions run on the server, and the browser calls both with full types.
kitr new tasks --kit fullstackFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
Server actions or API routes?
Both run on your server. Pick by who calls them:
| Use | When |
|---|---|
| Server actions | Only your own React app calls it. The quickest way: no routes, no client to set up. |
| API routes | Something else calls it too: a mobile app, a partner, a webhook. You get URLs, OpenAPI and a typed client. |
Many apps use actions for the UI and a handful of API routes for integrations.
A server action
kitr make:action tasks/renameFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
// app/actions/tasks/rename.ts
import { defineAction } from 'kitr/actions'
import { z } from 'zod'
export const renameTask = defineAction('tasks.rename', {
input: z.object({ id: z.string(), title: z.string().min(1) }),
effects: (task) => ({ invalidate: [['tasks', 'list']] }),
handler: async ({ input }) => db.tasks.update(input.id, { title: input.title }),
})
Call it from a form or a mutation. Validation failures reach the form's fields; the effect refreshes every task list:
const rename = useKitrMutation(defineMutation(renameTask.mutation()))
Live updates
With Realtime, one channel definition serves both ends. The server publishes; every open browser applies the event's cache effects:
// app/channels/tasks.ts
export const tasksChannel = defineChannel<{ board: string }>({
name: 'tasks',
events: { changed: { effects: ({ board }) => ({ invalidate: [['tasks', 'list', { board }]] }) } },
authorize: (session) => !!session,
})
Server rendering and SEO
SEO controls which pages render on the server and what goes in their <head>:
export const Route = createFileRoute('/tasks/$id')({
head: ({ loaderData }) => seo({ title: loaderData.title, description: loaderData.summary }),
})
Deploying
npm run build
npm start
npm start runs kitr start, which serves each platform's build (the site on srvx, the API on its runtime). The output is also a standard fetch handler, so platforms that run those directly work too.