TanStack Start
Scaffold a TanStack Start app with kitcn and Better Auth.
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
npx kitcn 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
npx kitcn devnpm run dev4. Key Files
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.