TanStack Start Production-Ready SaaS: What You Actually Need Before You Launch
Most tutorials show you how to run bun dev and call it a day. Getting TanStack Start to actually production-ready — handling real users, real payments, real auth sessions, and real errors — is a different game entirely. I've shipped two SaaS products on TanStack Start and learned exactly where the sharp edges are.
Here's my honest checklist.
TanStack Start IS Production-Ready — With Caveats
Let me be direct: TanStack Start is absolutely production-ready in 2026. The framework's server functions, type-safe routing, and seamless React Query integration make it one of the best choices for full-stack TypeScript SaaS. But "production-ready" doesn't mean "zero configuration after bun create."
You still need to wire up:
- Auth with real session persistence
- Payment webhooks that don't silently fail
- Error tracking so you know when things break
- Database migrations you can run without downtime
- A deployment target that won't bankrupt you
Let me walk through each one.
Auth: Sessions Need to Survive Restarts
The most common gotcha I see: auth sessions that vanish every time you redeploy. If you're using Better-Auth (which is what I recommend — see the setup guide here), make sure your session store is backed by your database, not in-memory.
Here's the config that actually works in production with Drizzle:
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "sqlite" for local dev
}),
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
updateAge: 60 * 60 * 24, // refresh daily
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5-minute client cache
},
},
emailAndPassword: {
enabled: true,
},
});
That cookieCache setting is the difference between "auth works" and "auth works without hammering your DB on every request." In production with real traffic, this matters.
Payments: Stripe Webhooks Are Your Source of Truth
This is where I've seen more production incidents than anywhere else. Your app processes a checkout — great. But what happens when Stripe tries to tell you about a failed renewal 48 hours later and your webhook endpoint returns a 500?
You need idempotent webhook handling. Full stop. I covered this in depth in Stripe Webhooks in TanStack Start, but the short version:
// src/routes/api/webhooks/stripe.ts
import Stripe from "stripe";
import { db } from "~/lib/db";
import { processedWebhooks } from "~/lib/schema";
export const POST = async ({ request }: { request: Request }) => {
const sig = request.headers.get("stripe-signature")!;
const body = await request.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
// Idempotency check — never process the same event twice
const existing = await db.query.processedWebhooks.findFirst({
where: (t, { eq }) => eq(t.stripeEventId, event.id),
});
if (existing) return new Response("Already processed", { status: 200 });
// Handle and record
await handleStripeEvent(event);
await db.insert(processedWebhooks).values({ stripeEventId: event.id });
return new Response("OK", { status: 200 });
};
Store processed event IDs. It's 5 minutes of work that prevents charging customers twice.
Database: Migrations Without Downtime
Drizzle's migration story is clean, but you need a production workflow. Here's what I run before every deploy:
# Generate the migration
bunx drizzle-kit generate
# Review it — always review migrations before running
cat drizzle/migrations/0012_*.sql
# Apply in production
bunx drizzle-kit migrate
The critical rule: never run drizzle-kit push in production. That's for development only. push skips migration files entirely and directly syncs your schema — you will lose data.
For zero-downtime migrations on Postgres, make additive changes only: add columns with defaults, never drop or rename columns in the same deploy that removes the old code.
Deployment: Pick Your Target Early
I've tried a few options. For TanStack Start in 2026, here's my honest ranking:
flowchart TD
A[Your TanStack Start App] --> B{Traffic expectations?}
B -->|Low to medium| C[Cloudflare Workers]
B -->|Heavy compute or DB| D[Fly.io / Railway]
B -->|Want zero ops| E[Vercel — but watch the bill]
C --> F[Fast, cheap, global edge]
D --> G[Persistent connections, great for Bun]
E --> H[Convenient, expensive at scale]
Cloudflare Workers with Bun is my default pick for new SaaS products — you get global edge distribution and you won't get a $300 invoice surprise. See the full deployment guide here.
For anything requiring persistent DB connections (Drizzle with a real Postgres instance), Railway or Fly.io are better fits.
Error Tracking: You Need to Know Before Users Email You
Don't ship without error tracking. The setup is 10 minutes but saves hours of debugging:
// src/entry-server.tsx — Sentry for TanStack Start
import * as Sentry from "@sentry/bun";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
});
Sentry has a generous free tier. There's no reason not to have it.
The Production Readiness Checklist
Here's the full list I run through before every launch:
| Item | Why It Matters |
|---|---|
| DB-backed sessions | Sessions survive redeploys |
| Webhook idempotency | No duplicate charges |
| Drizzle migrations (not push) | No accidental data loss |
| Environment variables validated | Fail fast on missing config |
| Error tracking (Sentry) | Know before users tell you |
| Stripe webhook secret verified | Reject spoofed events |
| Rate limiting on auth routes | Prevent brute force |
Health check endpoint /api/health |
Deployment rollback detection |
FAQ
Is TanStack Start stable enough for production in 2026? Yes. The core APIs are stable, it's being used in production by a growing number of teams, and the TanStack ecosystem (Query, Router, Form) is battle-tested. The biggest risk is ecosystem immaturity for niche integrations — but for a SaaS with standard auth, payments, and email, you're fine.
Can I use TanStack Start with a Postgres database in production? Absolutely. Pair it with Drizzle ORM and a managed Postgres on Neon, Railway, or Supabase. Drizzle's type-safe queries and migration tooling are excellent in production environments.
What's the best hosting for TanStack Start? Cloudflare Workers for edge-heavy apps, Fly.io or Railway if you need persistent server-side state. Avoid Vercel unless you enjoy surprise bills as you scale.
Do I need a separate API layer? No. TanStack Start's server functions and API routes handle everything you need. I've shipped full SaaS products with zero separate API services — all in one repo, all type-safe.
What about background jobs in TanStack Start? For simple tasks (email after signup, webhook processing), handle them inline in server functions. For heavier workloads, add a queue like BullMQ with your Bun runtime — it works well.
Going production-ready is less about TanStack Start specifically and more about doing the boring infrastructure work properly. The framework is solid. The rest is on you.
If you want to skip the auth plumbing, Stripe setup, and email wiring, BetterStarter ships with all of it pre-configured and production-tested — $99 one-time. You clone, configure your env vars, and ship. That's the point.
For more on the full stack, the TanStack Start tech stack breakdown is worth a read before you finalize your architecture choices.