TanStack Start
Scaffold a TanStack Start app with kitcn and Better Auth.
In this guide, we'll scaffold a fresh TanStack Start app with kitcn, layer in auth with the CLI, and then walk through the generated files so you know what the scaffold actually wrote. This page stays on the CLI path first. The file snippets later in the guide are reference output, not a separate manual install flow.
1. Scaffold
mkdir my-app
cd my-appnpx kitcn@latest init -t start --yespnpm dlx kitcn@latest init -t start --yesyarn dlx kitcn@latest init -t start --yesbunx --bun kitcn@latest init -t start --yesinit -t start creates the TanStack Start shell and layers in the kitcn baseline — backend files, client providers, local env scaffolding, and a starter messages demo.
| File | Purpose |
|---|---|
convex/functions/schema.ts | Starter schema |
convex/functions/messages.ts | Starter query and mutation |
convex/lib/crpc.ts | Server-side cRPC builders |
src/lib/convex/crpc.tsx | React cRPC context |
src/lib/convex/convex-provider.tsx | Convex + TanStack Query providers |
src/lib/convex/query-client.ts | QueryClient with hydration config |
src/components/providers.tsx | App-level providers |
2. Add Auth
Next, we'll add auth on top of the generated baseline. Keep using the published CLI runner for your package manager here as well.
npx kitcn@latest add auth --yespnpm dlx kitcn@latest add auth --yesyarn dlx kitcn@latest add auth --yesbunx --bun kitcn@latest add auth --yesThis scaffolds the auth layer on top of the baseline:
| File | Purpose |
|---|---|
src/lib/convex/auth-client.ts | Better Auth client with convexClient() plugin |
src/lib/convex/auth-server.ts | Server auth helpers (handler, getToken) |
src/lib/convex/server.ts | Caller factory with auth token support |
src/routes/api/auth/$.ts | Auth API endpoint |
convex/functions/auth.ts | Auth config and plugin registration |
convex/functions/http.ts | HTTP route with auth wired |
3. Run
Open two terminals. In the first terminal, start the backend with the published CLI runner for your package manager:
npx kitcn@latest devpnpm dlx kitcn@latest devyarn dlx kitcn@latest devbunx --bun kitcn@latest devIn the second terminal, start the app with your package manager's dev script:
npm run dev
# or pnpm dev
# or yarn dev
# or bun run devThe TanStack Start app serves on http://localhost:3000 by default. kitcn dev
starts the local Convex backend and keeps codegen up to date while you work.
4. Generated Files
The rest of this page is a reference for the files kitcn add auth --yes
generates. We'll start with the auth client and server helpers, then move
through the route and provider wiring.
Important: If you are following the CLI path, you should not need to type these files by hand. Use them to verify the scaffold output or to understand what each generated file is responsible for.
Auth Client
import { createAuthClient } from 'better-auth/react';
import { convexClient } from 'kitcn/auth/client';
import { createAuthMutations } from 'kitcn/react';
export const authClient = createAuthClient({
baseURL:
typeof window === 'undefined'
? (import.meta.env.VITE_SITE_URL as string | undefined)
: window.location.origin,
plugins: [convexClient()],
});
export const {
useSignInMutationOptions,
useSignOutMutationOptions,
useSignUpMutationOptions,
} = createAuthMutations(authClient);Auth Server
import { convexBetterAuthReactStart } from 'kitcn/auth/start';
export const {
handler,
getToken,
fetchAuthQuery,
fetchAuthMutation,
fetchAuthAction,
} = convexBetterAuthReactStart({
convexUrl: import.meta.env.VITE_CONVEX_URL!,
convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL!,
});Auth API Endpoint
import { createFileRoute } from '@tanstack/react-router';
import { handler } from '@/lib/convex/auth-server';
export const Route = createFileRoute('/api/auth/$' as never)({
server: {
handlers: {
GET: ({ request }) => handler(request),
POST: ({ request }) => handler(request),
},
},
});cRPC Context
import { api } from '@convex/api';
import { createCRPCContext } from 'kitcn/react';
export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
api,
convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL!,
});Providers
'use client';
import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
import { ConvexAuthProvider } from 'kitcn/auth/client';
import {
ConvexReactClient,
getConvexQueryClientSingleton,
getQueryClientSingleton,
useAuthStore,
} from 'kitcn/react';
import type { ReactNode } from 'react';
import { authClient } from '@/lib/convex/auth-client';
import { CRPCProvider } from '@/lib/convex/crpc';
import { createQueryClient } from '@/lib/convex/query-client';
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL!);
export function AppConvexProvider({
children,
}: {
children: ReactNode;
}) {
return (
<ConvexAuthProvider authClient={authClient} client={convex}>
<QueryProvider>{children}</QueryProvider>
</ConvexAuthProvider>
);
}
function QueryProvider({ children }: { children: ReactNode }) {
const authStore = useAuthStore();
const queryClient = getQueryClientSingleton(createQueryClient);
const convexQueryClient = getConvexQueryClientSingleton({
authStore,
convex,
queryClient,
});
return (
<TanstackQueryClientProvider client={queryClient}>
<CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
{children}
</CRPCProvider>
</TanstackQueryClientProvider>
);
}Server Caller
Use runServerCall to call cRPC procedures from TanStack Start server functions:
import { api } from '@convex/api';
import { getRequestHeaders } from '@tanstack/react-start/server';
import { createCallerFactory } from 'kitcn/server';
import { getToken } from '@/lib/convex/auth-server';
const { createContext, createCaller } = createCallerFactory({
api,
convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL!,
auth: {
getToken: async () => {
return {
token: await getToken(),
};
},
},
});
type ServerCaller = ReturnType<typeof createCaller>;
async function makeContext() {
const headers = await getRequestHeaders();
return createContext({ headers });
}
function createServerCaller(): ServerCaller {
return createCaller(async () => {
return await makeContext();
});
}
export function runServerCall<T>(fn: (caller: ServerCaller) => Promise<T> | T) {
const caller = createServerCaller();
return fn(caller);
}Usage:
import { runServerCall } from '@/lib/convex/server';
import { createServerFn } from '@tanstack/react-start';
export const getCurrentUser = createServerFn({ method: 'GET' }).handler(
async () => {
return await runServerCall((caller) => caller.user.getSessionUser({}));
},
);Root Route
The scaffold uses a simple root route — providers wrap the outlet:
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router';
import { Providers } from '@/components/providers';
import appCss from '../styles.css?url';
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'TanStack Start Starter' },
],
links: [{ rel: 'stylesheet', href: appCss }],
}),
component: RootComponent,
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
function RootComponent() {
return (
<Providers>
<Outlet />
</Providers>
);
}If you see an error about a missing VITE_ environment variable, add it to your local env file.