TanStack Start教程SaaSMVP

如何用 TanStack Start 在 7 天内搭建 SaaS MVP

Stefan

创始人兼开发者

用 TanStack Start 搭建 SaaS MVP 的 7 天计划——类型化路由、Better Auth、Drizzle on D1、Stripe 支付与 Cloudflare Workers 部署。

如何用 TanStack Start 在 7 天内搭建 SaaS MVP

这是一份 7 天执行路线图,手把手演示如何用 TanStack Start 从零搭建 SaaS:认证、数据库、支付、邮件与部署端到端接线,外加一个受保护路由和一个持久化的示例数据流程。它不是生产就绪的产品,也不是复制粘贴式的完整教程。每一天都写明"当天完成什么、不覆盖什么",让边界始终清晰。这份计划的价值是基础设施跑通、能承载业务代码,而不是产品本身。

如果你还在框架选型阶段,我们此前的对比文章解释了为什么 TanStack Start 适合 Cloudflare 原生 SaaS。关于时间:7 天是一个开发者在这个范围内务实的节奏,不是保证。快一点或慢一点都正常——计划的真正价值在于顺序与验收标准,而不是日历。

第 1 天 — 项目脚手架

完成:一个本地可运行的 TanStack Start 项目。不覆盖:框架底层原理,这一周里你会自然熟悉。

用官方 CLI 创建项目,它会引导你选择包管理器与可选插件:

npx @tanstack/cli@latest create

TanStack Start 目前处于 Release Candidate 阶段——按官方说明,功能已完整、API 已稳定。CLI 生成的项目自带 src/routes/ 文件路由结构,本 TanStack Start 教程后面的步骤都建立在它之上。

启动开发服务器,先确认结构就位再继续。请使用官方 CLI 而不是手写配置——生成的项目已经把 Vite、路由与 server entry 接好,这正是本计划后续步骤所依赖的配置。

验收标准npm run dev(或对应包管理器命令)能打开页面,修改路由文件能热更新。

第 2 天 — 路由与数据加载

完成:几个带 typed loader 的路由。不覆盖:高级渲染策略——MVP 用服务端渲染已经足够。

TanStack Start 构建在 TanStack Router 之上,路由是文件式的且类型安全:loader、路由参数、搜索参数全部带类型。建一个在 loader 里加载数据而不是在组件里请求的路由,页面就能从路由定义自动推导出数据的类型。

实际收益在重构时体现:重命名一条路由或改一个参数,所有被破坏的引用在编译期就会报错,而不是上线后才发现。对两周大的 MVP 代码库这听起来微不足道;六个月后,这就是"安全重命名"与"页面直接挂掉"的区别。loader 还提供了一个统一的加载与错误状态位置,数据在途时界面保持一致。

验收标准:存在两个路由,其中一个带 typed loader;参数名写错时类型检查失败。

第 3 天 — 数据库:Drizzle + Cloudflare D1 + 迁移

完成:Drizzle 接入 Cloudflare D1、schema 与迁移,外加一个小的持久化示例流程。不覆盖:查询优化与备份策略,都可以往后放。

Drizzle 用 TypeScript 定义 schema 并生成 SQL 迁移,不会额外引入运行时依赖,适合 serverless 环境。D1 是 Cloudflare 的 SQLite 数据库,在 Worker 中通过 DB 绑定访问。本计划先接数据库再接认证,因为 Better Auth 的用户表与会话表也要放在这里。

先把 D1 绑定加进 Cloudflare 配置,然后定义第一批表。保留一张示例表,让 MVP 有一个持久化数据流——比如 notes 表(idtitlebody):

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const notes = sqliteTable('notes', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  body: text('body'),
})

生成并执行迁移,然后通过一个服务端路由各接一次读写,让数据流端到端可见。因为 D1 兼容 SQLite,本地开发可以使用与生产相同的 schema 与驱动——差异只在于绑定指向哪个数据库。

验收标准:路由能从 D1 读、向 D1 写,数据在开发服务器重启后仍在。

第 4 天 — 认证:Better Auth

完成:邮箱密码注册与登录、会话,以及一个受保护路由。不覆盖:邮箱验证暂不做(依赖邮件服务,放到第 6 天);OAuth 是很好的扩展,但每个提供商都需要 Client ID、Client Secret 与回调 URL,不属于 MVP 核心。

Better Auth 官方支持 TanStack Start,并提供 Drizzle 适配器。先安装,设置 BETTER_AUTH_SECRET(至少 32 个字符)与 BETTER_AUTH_URL,然后创建认证实例并启用邮箱密码——同时加上 tanstackStartCookies 插件,由它负责 TanStack Start 的 cookie 设置(它必须是插件数组的最后一位):

import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { db } from './db'

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: 'sqlite' }),
  emailAndPassword: { enabled: true },
  plugins: [tanstackStartCookies()],
})

用官方 CLI 生成用户表与会话表:

npx auth@latest generate

然后沿用第 3 天的流程,为这些新增的 auth 表在本地再生成并应用一次 Drizzle 迁移,并在部署前把这次迁移应用到生产 D1——磁盘上的 schema 不迁移进库,数据库里就没有这些表。

按官方 TanStack Start 集成指南,把 handler 挂到 catch-all 服务端路由上,并创建客户端:

// src/routes/api/auth/$.ts
import { createFileRoute } from '@tanstack/react-router'
import { auth } from '@/lib/auth'

export const Route = createFileRoute('/api/auth/$')({
  server: {
    handlers: {
      GET: async ({ request }) => auth.handler(request),
      POST: async ({ request }) => auth.handler(request),
    },
  },
})

受保护路由用 beforeLoad 里的服务端函数检查会话,而不是客户端检查——这样客户端导航也会被拦截。

验收标准:新账户可以注册并登录,会话写入 D1,受保护路由能拦截未登录访客。

第 5 天 — 支付:完整的 Stripe 订阅闭环

完成:一条支付流程端到端跑通。不覆盖:Customer Portal、一次性购买与按用量计费——列为扩展。

选定订阅路径并闭环:

  1. 配置 Stripe 测试模式,为你的方案建一个测试价格
  2. 用户点击订阅时,由服务端路由创建 Checkout Session——价格 ID 从配置读取,绝不由客户端传入
  3. 用 Stripe CLI 本地转发 webhook——stripe listen 会打印该端点的 whsec_ 签名密钥
  4. 在 webhook 路由中校验签名,再信任任何事件
  5. checkout.session.completed 时记录订单并关联订阅;订阅状态由 customer.subscription.* 生命周期事件维护,且按事件幂等处理——重复投递的事件不会产生重复记录

签名校验是这一天的安全边界。在 Cloudflare Workers 上,需要用基于 fetch 的 HTTP 客户端与 subtle-crypto provider 创建 Stripe 客户端(Workers 没有 Node 的 crypto 模块),密钥从 Worker handler 收到的绑定里读取:

import Stripe from 'stripe'
import { env } from 'cloudflare:workers'

const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
  httpClient: Stripe.createFetchHttpClient(),
})

// 在 webhook handler 内;env 来自 Worker 的绑定:
const event = await stripe.webhooks.constructEventAsync(
  await request.text(),                            // Stripe 发送时的原始请求体
  request.headers.get('stripe-signature'),
  env.STRIPE_WEBHOOK_SECRET,
  undefined,
  Stripe.createSubtleCryptoProvider(),
)

边缘运行时请使用异步验证版本。两个细节决定它能否在生产环境工作:原始请求体要用 request.text() 在任何解析发生之前读取;密钥必须使用接收事件的那个端点对应的密钥。

验收标准:用测试卡订阅后,在已验签的订阅生命周期 webhook 处理完成后,D1 中出现一条有效的订阅记录,而不是在返回页模拟成功。

第 6 天 — 邮件与部署

完成:Resend 交易邮件——含 Better Auth 邮箱验证与购买确认邮件——以及 Cloudflare Workers 部署。不覆盖:邮件模板体系与多环境配置。

Resend 是面向开发者的邮件 API:验证域名、创建 API key 之后,发送就是一次 API 调用。今天启用 Better Auth 的邮箱验证,让用户收到的验证邮件是真实送达的,而不是桩代码。购买确认邮件由同一个已验签的 checkout.session.completed 事件触发——这个时点支付才被确认,所以它是确认邮件的正确触发源。customer.subscription.* 事件负责同步订阅生命周期状态,不是确认邮件的触发源。邮件发送要幂等:以 checkout 事件为键去重,重复投递的 webhook 不会发出重复邮件。

Cloudflare Workers 是 TanStack Start 的官方托管合作伙伴。安装 @cloudflare/vite-pluginwrangler,把插件加进 vite.config.ts,并创建带兼容性标志的 wrangler.jsonc

{
  "name": "tanstack-start-app",
  "compatibility_date": "2026-07-16",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry"
}

上面的兼容日期只是示例——创建文件时请使用与你的 Cloudflare 账户设置一致的近期日期。

然后登录并部署:

npx wrangler login
npm run deploy

数据库绑定与密钥按环境配置。把 Stripe webhook 密钥与 Resend key 放进 Cloudflare secrets,绝不放进客户端代码——第 3 天的 D1 绑定也在这里发挥生产价值。

验收标准:部署后的 Worker 正常服务应用,D1 绑定在生产环境可用,收件箱里收到验证邮件和购买确认邮件。

第 7 天 — 验证与打磨

完成:端到端验证与上线检查清单。不覆盖:i18n、主题与 SEO 深度优化——属于上线前增强项,不是 MVP 核心。

在部署后的 URL 上完整走一遍:注册 → 收到并点击验证邮件 → 登录 → 打开受保护路由 → 用测试卡订阅 → 确认 webhook 把订阅写入 D1 → 收到购买后的邮件。

然后检查运维基础项:密钥在 Cloudflare 而不在仓库里、生产环境的迁移已执行、开发与生产行为一致。只在本机正常的地方都要修掉。

验收标准:用全新账户在生产 URL 上完整跑通全流程。

常见错误

本计划里的调试时间经常集中在五类常见错误上:

  • 漏掉生产环境的 D1 迁移。 schema 只在本地存在,部署后的数据库没有表——所有查询都会报 table-not-found。迁移要按环境执行,并用真实查询验证。
  • 混淆 server/client 边界。 仅服务端代码——数据库访问、认证 handler、密钥——绝不能被客户端组件引入。TanStack Start 官方文档对边界的说明非常明确。
  • OAuth 回调 URL 配置错误。 之后接入社交登录时,回调地址必须与提供商登记的值完全一致,包括端口。不一致会表现为让人困惑的重定向错误。
  • 跳过 webhook 签名校验。 任何信任原始 webhook 体的端点都可能被伪造。始终把原始 body 与 Stripe-Signature 请求头传给 constructEventAsync
  • 把密钥放进客户端可读的地方。 服务商密钥与端点密钥属于 Cloudflare secrets,不该出现在打包后的客户端代码里。

7 天后你得到什么

一个可运行的技术型 MVP:类型化路由、D1 支撑的认证、真实的订阅闭环、交易邮件与 Workers 部署。这正是本"用 TanStack Start 搭建 SaaS"计划的落点——持久化、认证与支付都是真的,接下来几周写的是业务代码,而不是管道。

如果你希望直接从一个已经组装好的地基开始,我们的模板清单整理了主流的 TanStack Start 起步套件与模板。如果你想要一个无需自己组装、基于 TanStack Start 的 SaaS MVP,TANSHIP Template 把这些基础(Better Auth、三套支付适配器、Drizzle on D1、R2 存储、Resend 与积分计量的 AI 工作流)封装在一个开箱即配的代码库里。如实说明:TANSHIP Template 是我们开发并销售的产品,它出现在这里,正是为了本计划结尾所指向的场景。

参考来源