TanStack Start Boilerplate: From Zero to Production SaaS in 2026

What a production TanStack Start boilerplate actually contains in 2026 — routing, data, auth, billing, email, and the edge runtime — and how to evaluate one before you adopt.

Huifer
Huifer
August 24, 20268 min read
Also available in:Deutsch

Written by Huifer, solo developer and maintainer of TanStack Ship. I maintain TanStack Ship because I use it — twelve production SaaS apps and counting since 2024, all of them running on a TanStack Start boilerplate. Every layer below is one I have personally wired, debugged, replaced, or ripped out at 2 a.m. on a Sunday. There is no marketing puff and no roadmap speculation; if a layer is rough, I name it rough. TanStack Ship is my commercial product, and that bias is declared up front.

Verified sources: TanStack Start docs · TanStack Router docs · TanStack Query docs · Cloudflare D1 · Cloudflare R2 · Cloudflare Queues · Cloudflare Durable Objects · Stripe Subscriptions · Resend · Better-Auth · TanStack Ship GitHub

Last updated: 2026-08-24 · Changelog


TL;DR: A production TanStack Start boilerplate in 2026 contains seven layers: the framework (Start, Router, Query, Form), the data layer (Drizzle, D1, R2, Queues, Durable Objects), auth (Better-Auth), billing (Stripe), email (Resend), observability, and CI/CD. The boilerplate I reach for first is TanStack Ship because it ships all seven layers wired together. The honest limits I would still flag: long-running background jobs past the 15-minute queue cap, multi-region Durable Object fan-out, and shadcn/ui component drift between upgrades.


What a TanStack Start boilerplate actually is in 2026

A TanStack Start boilerplate is a pre-wired project that ships the boring-but-decisive parts of a SaaS so you can write only the parts that are actually your product. In 2026 that means at minimum seven layers: framework, data, auth, billing, email, observability, and CI/CD. The official TanStack Start template gives you the first layer only. A production TanStack Start boilerplate gives you all seven.

If you are learning TanStack Start, evaluating the framework, or building a one-off project, the official template is the right call. If you are building a paid SaaS solo and you need the boring infrastructure already wired, a production boilerplate is the difference between shipping in a weekend and shipping in a quarter. The full feature inventory lives on /features; the comparison against other starters is on /compare; if you are evaluating against ShipFast, Makerkit, or Supastarter, those breakdowns live on /alternatives. Pricing for the license is on /pricing.

This article walks every layer of a production TanStack Start boilerplate with the rationale, the code shape, and the limits I have hit in production.

The seven layers

I split the boilerplate into seven layers because that is the smallest number where each layer has a distinct owner and a distinct failure mode. The layers are not always perfectly orthogonal — billing calls into auth, auth calls into the data layer — but in production you debug layer by layer, and a boilerplate that does not have a clean layer split will eventually fight you.

The seven layers:

  • Framework: TanStack Start, Router, Query, Form
  • Data: Drizzle ORM, D1, R2, Queues, Durable Objects
  • Auth: Better-Auth wired into D1 + KV
  • Billing: Stripe Subscriptions, webhooks, customer portal
  • Email: Resend + React Email, queue-backed
  • Observability: Logpush, request IDs, error tracking
  • CI/CD: Wrangler, GitHub Actions, preview deploys

A boilerplate that ships fewer than seven is a starter, not a production TanStack Start boilerplate. That is not a value judgment; it is just the cutoff where the boring-but-decisive parts are wired.

The edge-native runtime

Every production TanStack Start boilerplate in 2026 runs on Cloudflare Workers by default. There is no Node.js runtime in the bundle and no node:fs polyfills to maintain. I have measured cold starts consistently under 50 ms on the 2026 release, with the 99th percentile under 110 ms on the heaviest production app (a B2B invoicing tool that handles PDF generation in-line). The runtime details are in the Cloudflare Workers docs.

The runtime layer also includes automatic region-aware routing. Customers in Tokyo hit the APAC colo; customers in Frankfurt hit the EU colo. There is no configuration on your end. I have not stress-tested this past 4,000 concurrent WebSocket connections on a single Durable Object; that is the next limit I will hit.

bash
# Initialize a TanStack Start boilerplate project
npm create tanstack-start@latest my-saas
cd my-saas
npm install

The data-layer features

The data layer is where a TanStack Start boilerplate makes its most opinionated choices. D1 is a serverless SQLite. Drizzle is the ORM. R2 is the blob store. Queues decouple work. Durable Objects handle stateful coordination.

Drizzle + D1

Every production TanStack Start boilerplate in 2026 uses Drizzle against D1. The schema lives in app/lib/db/schema.ts; the migrations are generated by drizzle-kit; the connection is pooled at the Worker level. I have benchmarked this against the same schema on Neon and Supabase — for read-heavy SaaS workloads with sub-100 ms targets, D1 wins on latency and on cost. For write-heavy OLTP, Neon is still the right call; I would not pretend otherwise.

The feature that matters most is the prepared-statement cache. Drizzle's prepared statements are reused across Worker invocations, which means a findUserById call has effectively zero cold-start cost after the first invocation. I have profiled this under load: 8-12 ms for a single D1 read, 18-25 ms for a write with an index hit. The full Drizzle patterns are in the Drizzle ORM docs.

typescript
// app/lib/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: text('id').primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})

export const subscriptions = sqliteTable('subscriptions', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull().references(() => users.id),
  stripeCustomerId: text('stripe_customer_id').notNull(),
  status: text('status').notNull(),
  lastSyncedAt: integer('last_synced_at', { mode: 'timestamp' }).notNull(),
})

R2, Queues, and Durable Objects

R2 is the file store. There is no egress fee. I ship this in any app that touches user uploads — avatars, CSV imports, PDF exports. Signed URLs are minted server-side per request, never cached. The full pattern is in the Cloudflare R2 docs.

Queues are for retry-prone work — email, webhooks, exports. Durable Objects are for stateful coordination — rate limits, presence, real-time chat. Both are first-class in a production boilerplate; the only thing you have to do is pick the right primitive. I have a postmortem on D1 write-lock contention that shows what happens when you reach for D1 instead of a Durable Object for write-heavy coordination.

The auth and billing features

Auth and billing are the two layers that bleed the most hours for solo founders. A production TanStack Start boilerplate ships both wired together so you do not have to write either.

Better-Auth wired into D1 + KV

Auth is the usual time-sink. The boilerplate ships Better-Auth wired into D1 for the user table and Cloudflare KV for the session cache. The session middleware is a beforeLoad hook on every authenticated route. The full session lifecycle — login, refresh, logout, "remember me" — is already wired. I have shipped this in three different apps without ever touching the auth code.

The honest limit: the session TTL is hard-coded to 30 days. If you need a configurable session policy (some enterprise customers do), you have to write the override yourself. I do not consider this a defect; I consider it a deliberate scope decision. Sessions in production are almost never a place to be clever. The full Better-Auth API is in the Better-Auth docs.

Stripe Subscriptions + webhook idempotency

The billing module wraps Stripe Subscriptions with a single source of truth in D1. The local subscriptions table mirrors Stripe state on every webhook, with a last_synced_at column that lets you detect drift. The customer portal link is generated server-side and rotated on each session; I have never had a portal link leak in production.

The meter is the part most starters skip. A production boilerplate ships a usage meter that batches events into a single Stripe usage_record call at the end of each billing period. I have run this on a usage-priced AI API product for six months; the metering drift is under 0.3% versus Stripe's authoritative number. The full Stripe patterns are in the Stripe Subscriptions docs.

The developer-experience features

The DX layer is what makes a production boilerplate feel less like a starter and more like a framework. Three features matter: file-based routing, server functions, and the shadcn/ui starter.

File-based routing

TanStack Router generates types from your file structure. Every route parameter, every search param, every loader return is typed end-to-end. I have built routing systems with 200+ routes and the type inference has caught 100% of my typos at compile time since 2023. The full type-safe routing pattern is in the TanStack Router docs.

Server functions

Server functions are the only sanctioned way to touch the database, call Stripe, or send email. They never ship to the client bundle. I have written 200+ of them across my apps; zero have leaked secrets to the client. The pattern is documented in the TanStack Start server-functions docs.

shadcn/ui starter

The shadcn/ui starter is an opinionated copy-paste of 24 components. They are owned copies — you can edit them. The drift problem is real: when shadcn ships a new release, your local copies are stale. I have a CI step that diffs against the upstream registry and pings me when drift exceeds 30 days. The honest limit is that this drift step is a TanStack Ship internal script, not a public module.

Honest limits and what's still rough

A boilerplate list without a limits section is marketing. Here is the honest one.

The limits I have hit in production

  • Queue cap at 15 minutes. Cloudflare Queues are great for retries under 15 minutes. Anything longer (a 45-minute PDF export job) needs a different pattern. I currently chunk the work or push the customer to a polling UI.
  • Durable Object hot partitions. A single Durable Object is single-tenant. If you use a DO as a per-tenant rate limiter and one tenant goes viral, that DO is a hot partition. I have hit this once; the fix was to shard by (tenantId, minute) and accept 60 seconds of rate-limit imprecision.
  • shadcn drift. The 24 owned copies are yours to maintain. The drift step is internal.

What I would still build

  • A first-class usage-based billing meter with a UI (I have the API, not the UI)
  • A multi-region Durable Object fan-out pattern (I have a draft, not a ship)
  • A WebSocket component library that survives Worker restarts (Durable Objects handle this, but the API is rough)

The full feature inventory is on /features. The license tiers are on /pricing. The side-by-side against other starters is on /compare and the per-competitor breakdowns are on /alternatives.


A production TanStack Start boilerplate is the difference between shipping a paid SaaS in a weekend and shipping in a quarter. The seven layers — framework, data, auth, billing, email, observability, CI/CD — are the layers I reach for first. The full module list is on /features, the pricing is on /pricing, and the comparison against other starters is on /compare. If you have questions, the changelog is on /changelog and the rest of the writing is on /blog.