kitcn

TanStack Start

Scaffold a TanStack Start app with kitcn and Better Auth.

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

npx kitcn 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

terminal 1
npx kitcn dev
terminal 2
npm run dev

4. Key Files

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