Getting Started with TanStack Start and Convex (2026 Guide)
Step-by-step guide to getting started with TanStack Start and Convex: scaffold a full-stack React SaaS and wire up the realtime Convex backend in 2026.

TanStack Start
Part 1 of 1
This is the first article in our series on building a modern SaaS application with TanStack Start and Convex. TanStack Start is now at v1 Release Candidate, feature-complete with a stable API, and the tooling has changed significantly since the original 2025 version of this guide. By the end of this article you’ll have a running full-stack React app with SSR, file-based routing, and a realtime Convex backend.
What changed since 2025
The TanStack Start ecosystem went through several breaking changes:
- Project structure:
app/directory moved tosrc/ - Dev server: vinxi replaced by a native Vite plugin (built on Nitro)
- Package rename:
@tanstack/startis now@tanstack/react-start - Head management:
Metafrom@tanstack/startreplaced byHeadContentfrom@tanstack/react-router
This guide reflects the current (2026) state of the framework.
What is TanStack Start?
TanStack Start is a full-stack React framework built on top of TanStack Router. It comes from the TanStack team (the same people behind TanStack Query, Router, Table, and other widely-used libraries). Key features:
- File-based routing: similar to Next.js or Remix, routes map to files in
src/routes/. - Server-side rendering (SSR) with client-side hydration.
- Integrated data fetching via TanStack Query hooks.
- Vite tooling (or Rsbuild 2 as an alternative) for fast dev and builds.
- Nitro-powered server output: deploy to any Node.js host, serverless platform, or VPS.
TanStack Start reached v1.0 Release Candidate in September 2025 and ships 1.x npm versions. The API is considered stable. If you want to see how TanStack Start compares to Next.js and Astro, we have a dedicated comparison.
What is Convex?
Convex is a realtime backend-as-a-service platform. It handles your database, serverless functions, and client sync so you can focus on frontend code. You can also build a real-time app with Convex using other frameworks.
- Realtime database: automatically syncs data changes to connected clients
- Serverless functions: write queries (read) and mutations (write) in TypeScript, hosted by Convex
- End-to-end type safety: shared types between frontend and backend
- Web dashboard: manage data, view logs, and debug from docs.convex.dev/dashboard
- File storage, scheduled tasks, and vector search built in
Convex integrates with TanStack Start via the @convex-dev/react-query package, which lets you fetch and mutate data using standard TanStack Query hooks.
Prerequisites
Before you start, make sure you have:
- Node.js >= 20.19 (Node 22 LTS recommended). The
@tanstack/react-routerpackage requires this as a minimum. If you’re on Node 18, you’ll hit engine errors on install. - npm (comes with Node)
- A terminal and code editor
- A Convex account is optional for local development — Convex runs locally without one. You can link to a cloud account later with
npx convex login.
No Docker, no VPS, no DNS needed for this tutorial. Everything runs locally.
Installation: scaffold with Convex
The fastest way to get started is the Convex template, which scaffolds a project with TanStack Start and Convex pre-wired:
npm create convex@latest -- -t tanstack-start my-saas-app
cd my-saas-appThis creates a project with React 19, TypeScript, TanStack Start, and Convex client setup already configured. The convex/ directory comes with a schema file and the generated types.
npx create-start-app@latest
cd my-app
npm install convex @convex-dev/react-query @tanstack/react-router-with-query @tanstack/react-queryThe manual path gives you a bare TanStack Start project. You’ll need to wire Convex into src/router.tsx yourself (covered in the router section below). See the TanStack Start build-from-scratch docs for more details.
Verify the scaffold worked:
cat package.json | grep -E "tanstack|convex"
You should see @tanstack/react-start, @tanstack/react-router, @tanstack/react-query, convex, and @convex-dev/react-query in the output.
Failure mode: If npm create convex fails, check your Node version (node -v) and npm registry connectivity (npm ping).
Starting the development environment
Run the dev server:
npm run dev
Under the hood, this executes convex dev --start 'vite dev' — the Convex CLI drives Vite directly. On first run, Convex will prompt you to name your project and bootstrap a local backend:
? Choose a name: my-saas-app
✔ Downloaded Convex backend binary
✔ Started running a deployment locally at http://127.0.0.1:3210
name as CONVEX_DEPLOYMENT to .env.local
URL as VITE_CONVEX_URL to .env.local
➜ Local: http://localhost:3000/
✔ Convex functions ready! (839.54ms)
Verify it worked
- Open
http://localhost:3000in your browser — you should see the default page. - Confirm “Convex functions ready” appears in your terminal.
- Check that
.env.localcontainsVITE_CONVEX_URL:
cat .env.local | grep VITE_CONVEX_URLWhat’s happening:
- Convex initialization:
convex devsets up a local backend, savesVITE_CONVEX_URLandCONVEX_DEPLOYMENTto.env.local. These connect your frontend to the Convex backend. - Vite dev server: Your app runs at
http://localhost:3000. - Convex dashboard: Run
npx convex dashboardto open the web UI for managing data, viewing logs, and testing functions.
Tip: Run npx convex login to link your local deployment to a Convex account for cloud syncing and deployment.
Failure modes:
- Missing
VITE_CONVEX_URL: If the frontend can’t connect, restartnpm run devafter the firstconvex devbootstraps the env file. - Port 3000 in use: Vite auto-picks another port (check terminal output), or set
server.portinvite.config.ts.
Understanding the project structure
The scaffold creates this layout (note: src/, not app/):
.
├── src/
│ ├── routes/ # Page routes
│ │ ├── __root.tsx # Root layout (HTML structure, global providers)
│ │ └── index.tsx # Homepage component ('/')
│ ├── router.tsx # Router setup (integrates Convex + Query)
│ ├── routeTree.gen.ts # Auto-generated route tree
│ ├── start.ts # App entry point (replaces old client.tsx + ssr.tsx)
│ └── styles/
│ └── app.css # Global styles
├── convex/ # Backend logic
│ ├── schema.ts # Database schema (defines tables)
│ ├── _generated/ # Auto-generated types and API
│ └── ... # Your queries and mutations
├── public/ # Static assets
├── .env.local # Environment variables (VITE_CONVEX_URL)
├── vite.config.ts # Vite configuration
├── convex.config.ts # Convex config
├── package.json
└── tsconfig.json
Key files:
src/router.tsx— creates the TanStack Router instance and wires up Convex + Query client. This is the integration point.src/routes/__root.tsx— defines the root layout (<html>,<head>,<body>) and renders<Outlet />for child routes.src/routes/index.tsx— the homepage, usescreateFileRoute('/').src/start.ts— the single entry point that replaces the oldclient.tsx+ssr.tsx.convex/schema.ts— defines your database tables. See the Convex schema docs..env.local— storesVITE_CONVEX_URL. Never commit this file to Git.
If you see an app/ directory
If your scaffold has app/routes/, app/router.tsx, app/client.tsx, and app/ssr.tsx, you’re following outdated docs. The correct root is now src/. Re-scaffold with the latest template.
How the router and Convex client wire together
The integration happens in src/router.tsx. Here’s the current template code:
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { ConvexQueryClient } from '@convex-dev/react-query'
import { ConvexProvider } from 'convex/react'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL!
if (!CONVEX_URL) console.error('missing envar VITE_CONVEX_URL')
const convexQueryClient = new ConvexQueryClient(CONVEX_URL)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
gcTime: 5000,
},
},
})
convexQueryClient.connect(queryClient)
const router = routerWithQueryClient(
createRouter({
routeTree,
defaultPreload: 'intent',
context: { queryClient },
scrollRestoration: true,
defaultPreloadStaleTime: 0,
Wrap: ({ children }) => (
<ConvexProvider client={convexQueryClient.convexClient}>
{children}
</ConvexProvider>
),
}),
queryClient,
)
return router
}
What’s happening:
ConvexQueryClientis constructed fromVITE_CONVEX_URL— it bridges Convex functions with TanStack Query.QueryClientis configured with Convex’shashFnandqueryFnso queries route through Convex automatically.routerWithQueryClientfrom@tanstack/react-router-with-queryconnects the router and query client for SSR-aware data fetching.ConvexProviderwraps the app via theWrapprop, making the Convex client available to all components.defaultPreloadStaleTime: 0lets React Query own caching — this prevents double-fetches and stale data when usingrouterWithQueryClient.
Why two integration packages?
There are two co-existing packages for wiring TanStack Router with TanStack Query:
@tanstack/react-router-with-query— used by the official Convex template. ProvidesrouterWithQueryClient().@tanstack/react-router-ssr-query— used in the Convex quickstart docs. ProvidessetupRouterSsrQueryIntegration().
Both work. This guide follows the template approach (router-with-query) since it’s what you get from the scaffold. If you’re reading the Convex docs and see the other package, don’t be confused — they solve the same problem with slightly different APIs.
Basic routing with TanStack Router
TanStack Start uses file-based routing via TanStack Router. Two files define the routing foundation:
src/routes/index.tsx — the homepage:
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: Home,
})
function Home() {
return (
<div>
<h1>Welcome to My SaaS App</h1>
<p>Edit this page in <code>src/routes/index.tsx</code></p>
</div>
)
}
src/routes/__root.tsx — the root layout:
// src/routes/__root.tsx
import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
Key points:
createFileRoute('/')maps a file to a URL path.createRootRouteWithContextsets up the root layout with typed context (thequeryClientfrom the router).HeadContent(replaces the oldMeta) injects metadata into<head>.Scriptsinjects JavaScript bundles at the end of<body>.Outletrenders the current route’s component.
Adding a header and footer
Let’s add a header with navigation and a footer to src/routes/__root.tsx. This gives every page consistent branding:
Breaking change from 2025
The old Meta import from @tanstack/start is gone. Use HeadContent from @tanstack/react-router instead. ScrollRestoration is now handled by scrollRestoration: true on the router config — no explicit component needed.
// src/routes/__root.tsx
import {
createRootRouteWithContext,
HeadContent,
Link,
Outlet,
Scripts,
} from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
notFoundComponent: () => (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h1>404 - Page Not Found</h1>
<Link to="/">Go Home</Link>
</div>
),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<header
style={{
padding: '1rem',
borderBottom: '1px solid #eee',
background: '#f8f9fa',
position: 'sticky',
top: 0,
zIndex: 10,
}}
role="banner"
>
<nav aria-label="Main navigation">
<Link
to="/"
style={{
marginRight: '1rem',
fontWeight: 'bold',
color: '#333',
textDecoration: 'none',
}}
aria-label="Home page"
>
My SaaS App
</Link>
</nav>
</header>
<main style={{ padding: '1rem', minHeight: '80vh' }} role="main">
<Outlet />
</main>
<footer
style={{
padding: '1rem',
borderTop: '1px solid #eee',
textAlign: 'center',
background: '#f8f9fa',
}}
role="contentinfo"
>
<p>© {new Date().getFullYear()} My SaaS App. All rights reserved.</p>
</footer>
</RootDocument>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
What changed from the 2025 version:
Metareplaced byHeadContent(imported from@tanstack/react-router, not@tanstack/start).ScrollRestorationremoved — handled byscrollRestoration: truein the router config.notFoundComponentadded to fix the 404 warning.- The
MyRouterContextinterface is simpler — the Convex client is wrapped viaConvexProviderin the router’sWrapprop, so it doesn’t need to be in the context type.
Failure mode: If HeadContent import fails, you’re importing from @tanstack/start instead of @tanstack/react-router. Update the import.
Note on styling: This example uses inline styles for simplicity. The Convex template now ships with Tailwind v4, which you can use instead. Later articles in this series will introduce Radix UI components.
Verify your setup (build and smoke test)
Run a production build to catch type errors and confirm everything compiles:
npm run build
This runs npm run typecheck && vite build and outputs to .output/server/index.mjs (Nitro format). Then test the production server:
npm run start
Open the URL shown in the terminal. The app should render identically to dev mode.
Also verify the Convex backend:
npx convex dashboard
This opens the Convex dashboard in your browser. You should see your project listed with its functions.
You're done!
You now have a running full-stack app with:
- SSR via TanStack Start (Nitro-powered)
- File-based routing via TanStack Router
- Realtime Convex backend wired through TanStack Query
- A root layout with header, footer, and 404 handling
- A production build that compiles without errors
Failure mode: If npm run build fails with TypeScript errors, run npm run typecheck first to isolate them. If you get OOM errors on a small VPS, increase Node memory: NODE_OPTIONS=--max-old-space-size=4096 npm run build.
Convex pricing and deployment notes
Convex has a generous free tier that’s enough for prototyping and early development:
Cost breakdown
Free plan ($0):
- 1M function calls/month
- 20 GB-hours action compute
- 0.5 GB database storage
- 1 GB file storage
- 40 deployments
- 1,000 concurrent sessions
Professional ($25/developer/month):
- 25M function calls included
- Daily backups
- Custom domains
- Log streaming
Business & Enterprise: $2,500/month minimum.
Pricing as of August 2026 — check convex.dev/pricing for current numbers.
Deployment architecture: Convex is a managed cloud backend — your Convex functions and database run in Convex’s cloud. The TanStack Start frontend can deploy to any Node.js host or Nitro-compatible platform. If you want to deploy your TanStack Start app on your own VPS, Dokploy is a solid option on something like Hetzner Cloud.
For deploying the Convex side to production, see our guide on how to deploy a Convex app to production. If you prefer keeping everything on your own infrastructure, you can look into self-hosting Convex — we also have benchmarks comparing Convex self-hosted vs cloud free tier performance.
Troubleshooting common issues
Missing VITE_CONVEX_URL
Symptom: Frontend can’t connect to Convex. Console shows missing envar VITE_CONVEX_URL.
Fix: Restart npm run dev. The first convex dev run bootstraps .env.local with the URL. If you started the frontend before Convex finished initializing, the variable won’t be available yet.
Verify: cat .env.local | grep VITE_CONVEX_URL should show a URL like http://127.0.0.1:3210.
app/ vs src/ confusion
Symptom: You’re creating files in app/routes/ but nothing works.
Fix: The project structure moved from app/ to src/. If you’re following an older tutorial or the 2025 version of this article, your files should go in src/routes/, src/router.tsx, etc. Re-scaffold with the latest template if unsure.
Vite plugin ordering errors
Symptom: Build fails with cryptic errors related to Vite plugins.
Fix: In vite.config.ts, viteReact() (or @vitejs/plugin-react) must come after tanstackStart() in the plugins array:
plugins: [
tsConfigPaths({ projects: ['./tsconfig.json'] }),
tanstackStart(), // first
viteReact(), // after tanstackStart
],verbatimModuleSyntax errors
Symptom: Server bundles leaking into client bundles, or import errors that don’t make sense.
Fix: TanStack Start docs warn that verbatimModuleSyntax in tsconfig.json “can result in server bundles leaking into client bundles.” Keep it disabled (set to false or remove it).
Port 3000 already in use
Symptom: EADDRINUSE error when starting the dev server.
Fix: Vite will auto-pick another port — check the terminal output. To force a specific port, add server: { port: 3001 } to vite.config.ts. Or kill the process using port 3000: lsof -ti:3000 | xargs kill.
Convex dashboard not loading
Symptom: The dashboard URL in the terminal doesn’t open or shows an error.
Fix: Run npx convex dashboard manually. If you’re on a remote server, the dashboard binds to localhost by default — you’ll need an SSH tunnel or firewall rule to access it from your local machine.
notFoundError warning in terminal
Symptom: Warning: A notFoundError was encountered on the route with ID "__root__", but a notFoundComponent option was not configured...
Fix: Add notFoundComponent to your root route definition (see the header/footer section above). This provides a custom 404 page instead of the default error.
What’s next in this series
Now that the foundation is in place, the next article will cover writing your first Convex query and mutation — using the tasks example from the quickstart. You’ll learn how to define a schema, write server functions, and fetch data with useSuspenseQuery and convexQuery from @convex-dev/react-query.
The series will also cover Clerk authentication, Radix UI components, and Polar.sh payments.
Build a full todo app with TanStack Start

