TanStack Start Tutorial: Build a Full-Stack SaaS App in 2026
Most full-stack framework tutorials start with "create a new project" and end with a toy to-do app. That's not what I'm doing here. I'm going to show you exactly how TanStack Start works — the routing, the server functions, the data loading — because once it clicks, you'll wonder how you lived without it.
I've shipped two SaaS apps on TanStack Start this year. I'm not going back to Next.js.
What Makes TanStack Start Different
TanStack Start is a full-stack TypeScript framework built on top of TanStack Router. That distinction matters. Most frameworks treat routing as an afterthought — something you bolt on. TanStack Router was designed first as the best router in the React ecosystem, and Start is the full-stack layer on top.
What you get out of the box:
- File-based routing that's actually type-safe end-to-end
- Server functions (called with
createServerFn) — not API routes, not server actions with magic strings, just typed async functions that run on the server - Streaming + Suspense support built in
- Runs on Bun — startup time is negligible, and it feels snappy in a way Node.js apps never did for me
If you want a deeper comparison, I wrote a full breakdown in TanStack Start vs Next.js for SaaS.
Project Structure (The Non-Confusing Version)
Here's what a real TanStack Start SaaS project looks like:
my-saas/
├── app/
│ ├── routes/
│ │ ├── __root.tsx # Root layout — auth provider, theme, etc.
│ │ ├── index.tsx # "/" route
│ │ ├── dashboard/
│ │ │ ├── index.tsx # "/dashboard"
│ │ │ └── settings.tsx # "/dashboard/settings"
│ │ └── api/
│ │ └── webhook.ts # "/api/webhook" — for Stripe, etc.
│ ├── server/
│ │ ├── auth.ts # Better-Auth config
│ │ └── db.ts # Drizzle ORM instance
│ └── components/
├── content/blog/ # MDX blog posts
├── drizzle/ # DB migrations
├── package.json # "runtime": "bun"
└── app.config.ts # TanStack Start config
Every route file exports a Route created with createFileRoute. That's your entry point for loaders, actions, and the component itself.
File-Based Routing and Loaders
Here's a real dashboard route with a data loader:
// app/routes/dashboard/index.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { getSession } from '~/server/auth'
import { getUserProjects } from '~/server/queries'
export const Route = createFileRoute('/dashboard/')({
beforeLoad: async ({ context }) => {
const session = await getSession()
if (!session) throw redirect({ to: '/login' })
return { session }
},
loader: async ({ context }) => {
const projects = await getUserProjects(context.session.user.id)
return { projects }
},
component: DashboardPage,
})
function DashboardPage() {
const { projects } = Route.useLoaderData()
return (
<div>
{projects.map(p => <ProjectCard key={p.id} project={p} />)}
</div>
)
}
The beforeLoad runs before the loader — perfect for auth checks. The loader runs on the server and streams data down. No useEffect, no loading state, no waterfall. It just works.
Server Functions: The Killer Feature
Server functions are what sold me on TanStack Start. They look like regular async functions but run server-side. Full type safety, no API boilerplate.
// app/server/projects.ts
import { createServerFn } from '@tanstack/start'
import { db } from './db'
import { projects } from './schema'
export const createProject = createServerFn({ method: 'POST' })
.validator((data: { name: string; userId: string }) => data)
.handler(async ({ data }) => {
const [project] = await db
.insert(projects)
.values({ name: data.name, userId: data.userId })
.returning()
return project
})
Call it from your component like a normal function:
const newProject = await createProject({ data: { name, userId } })
No fetch, no JSON serialization, no endpoint to define. The framework handles all of it. This is the paradigm shift — you stop thinking in HTTP and start thinking in functions.
The Full-Stack Architecture
Here's how all the layers fit together in a typical TanStack Start SaaS:
graph TD
A[Browser - React Component] -->|useLoaderData| B[Route Loader]
A -->|createServerFn call| C[Server Function]
B --> D[Drizzle ORM]
C --> D
D --> E[(PostgreSQL / SQLite)]
A -->|Session cookie| F[Better-Auth]
F --> D
C -->|Stripe API| G[Stripe]
C -->|Email| H[Plunk Email]
The browser talks to the server through loaders (for reads) and server functions (for mutations). Drizzle handles the DB layer. Better-Auth manages sessions. Stripe and email are called from server functions — never from the client.
For setting up auth in this stack, I have a detailed walkthrough in TanStack Start Auth Setup.
Adding Stripe (The Right Way)
Stripe webhooks need a raw request body, which means a dedicated API route — not a server function. Here's the pattern:
// app/routes/api/webhook.ts
import { createAPIFileRoute } from '@tanstack/start/api'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export const APIRoute = createAPIFileRoute('/api/webhook')({
POST: async ({ request }) => {
const body = await request.text()
const sig = request.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
switch (event.type) {
case 'checkout.session.completed':
// provision access
break
}
return new Response('ok')
},
})
I wrote a complete guide on this in Stripe Webhooks in TanStack Start — it covers the full webhook lifecycle including subscription updates and cancellations.
FAQ
Is TanStack Start production-ready in 2026? Yes. It's been stable for over a year and the ecosystem around TanStack Router (the core) is battle-tested. Several indie projects and small SaaS products are running on it in production today, including BetterStarter itself.
Do I need to know TanStack Router to use TanStack Start?
You'll learn both simultaneously — Start is built on Router. The core concepts (file-based routes, loaders, beforeLoad) are the same. The learning curve is gentler than it sounds.
Can I use TanStack Start with Drizzle ORM and a real database? Absolutely. Drizzle + TanStack Start is one of the cleanest combos in the TypeScript ecosystem. You define your schema, run migrations, and query directly from loaders and server functions — no extra adapter layer.
How does TanStack Start compare to Remix? Both have great loader patterns. TanStack Start wins on type safety (end-to-end) and ecosystem (TanStack Query integration is seamless). Remix wins on maturity and community size. For greenfield SaaS in 2026, I'd pick TanStack Start.
Does TanStack Start support SSR and static generation? SSR is first-class. Static generation support is limited — TanStack Start is optimized for server-rendered, dynamic apps. For a static marketing site, use something else. For a real SaaS, it's perfect.
If you want all of this pre-wired — TanStack Start, Drizzle, Better-Auth, Stripe, Plunk email — without spending a weekend on setup, BetterStarter ships all of it as a production-ready boilerplate for $99 one-time. You clone it, configure your env vars, and you're building your actual product on day one.