kitcn

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-app
npx kitcn@latest init -t start --yes
pnpm dlx kitcn@latest init -t start --yes
yarn dlx kitcn@latest init -t start --yes
bunx --bun kitcn@latest init -t start --yes

init -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.

FilePurpose
convex/functions/schema.tsStarter schema
convex/functions/messages.tsStarter query and mutation
convex/lib/crpc.tsServer-side cRPC builders
src/lib/convex/crpc.tsxReact cRPC context
src/lib/convex/convex-provider.tsxConvex + TanStack Query providers
src/lib/convex/query-client.tsQueryClient with hydration config
src/components/providers.tsxApp-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 --yes
pnpm dlx kitcn@latest add auth --yes
yarn dlx kitcn@latest add auth --yes
bunx --bun kitcn@latest add auth --yes

This scaffolds the auth layer on top of the baseline:

FilePurpose
src/lib/convex/auth-client.tsBetter Auth client with convexClient() plugin
src/lib/convex/auth-server.tsServer auth helpers (handler, getToken)
src/lib/convex/server.tsCaller factory with auth token support
src/routes/api/auth/$.tsAuth API endpoint
convex/functions/auth.tsAuth config and plugin registration
convex/functions/http.tsHTTP 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 dev
pnpm dlx kitcn@latest dev
yarn dlx kitcn@latest dev
bunx --bun kitcn@latest dev

In the second terminal, start the app with your package manager's dev script:

terminal 2
npm run dev
# or pnpm dev
# or yarn dev
# or bun run dev

The 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

src/lib/convex/auth-client.ts
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

src/lib/convex/auth-server.ts
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

src/routes/api/auth/$.ts
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

src/lib/convex/crpc.tsx
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

src/lib/convex/convex-provider.tsx
'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:

src/lib/convex/server.ts
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:

src/functions/get-current-user.ts
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:

src/routes/__root.tsx
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.

Next Steps

On this page