TanStack DB Web Starter 实战:用 Electric Shape Proxy 模式构建认证安全的实时同步应用
2026/9/15 22:21:53 网站建设 项目流程

TanStack DB Web Starter 实战:用 Electric Shape Proxy 模式构建认证安全的实时同步应用

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

本篇技术指南以 Electric 仓库中的 tanstack-db-web-starter 示例 为主体,讲解如何将 TanStack Start(全栈 React 框架)、TanStack DB(客户端嵌入式数据库)、Better Auth(会话认证)、tRPC(类型安全变更)与 Electric(Postgres 实时同步引擎)组合成一个认证安全、实时同步、乐观更新的现代 Web 应用。读完本文,你将掌握 Shape Proxy 认证模式的完整实现原理、从零新增一张可同步表的七个标准步骤,以及本地开发中 Caddy 提供 HTTP/2 加速 shape 交付的完整机制。

技术栈与核心架构总览

该 Starter 解决的核心问题,是把三套互补的技术拼接成一个自洽的实时应用骨架:

技术职责仓库中的体现
TanStack Start全栈 React 框架(文件式路由 + 服务端路由处理器)路由定义
TanStack DB客户端嵌入式数据库:实时查询、跨集合 join、本地写collections.ts
ElectricPostgres 同步引擎,以 "shape"(表的过滤视图)形式经 HTTP 向客户端增量推送数据docker-compose.yaml
Better Auth会话认证(Cookie),负责登录、鉴权auth.ts
tRPC类型安全的写路径(变更),服务端二次校验权限trpc.ts
Drizzle ORM数据库建模与迁移(Zod schema 自动派生)schema.ts

数据流向可以概括为"读走 Electric、写走 tRPC":

  • 读路径:浏览器中的 TanStack DB 通过 Electric Shape Client 订阅 shape(指向/api/xxx代理端点),服务端代理校验会话后转发给 Electric 服务,Electric 从 Postgres 拉取逻辑复制变更后推送给客户端。
  • 写路径collection.insert()先在本地乐观更新,随后调用 tRPC 变更,服务端在事务中再次校验所有权并写入数据库,最后通过返回的事务 ID(txid)驱动本地乐观状态与后端同步收敛。

快速开始

前置条件

运行本项目需要三样东西:

  • Docker:用于运行 docker-compose.yaml 中定义的 Postgres 与 Electric 服务。
  • Caddy:提供本地 HTTPS,从而启用 HTTP/2 多路复用。安装后执行caddy trust(可能需要 sudo)信任其本地根证书。
  • Node 与 pnpm:示例仓库的package.json声明engines要求 Node>=20.19.0 || >=22.12.0

为什么需要 Caddy?Electric 的 shape 交付从HTTP/2 多路复用中显著获益:HTTP/2 允许同一个连接上并发加载多个 shape,而 HTTP/1.1 下浏览器对同一域名只允许 6 个并发连接,每个 shape 订阅占用一个连接时会形成瓶颈,表现为 shape 加载缓慢。HTTP/2 要求 HTTPS,因此本地开发需要 Caddy 承担反向代理。Vite 开发服务器本身只跑 HTTP/1.1,Caddy 负责升级连接(详见下文"关于 Caddy"一节)。

创建项目

基于该 Starter 创建新项目:

npx gitpick electric-sql/electric/tree/main/examples/tanstack-db-web-starter my-tanstack-db-project cd my-tanstack-db-project

复制环境变量模板:

cp .env.example .env

Tip.env中的值可按需编辑。默认值面向本地 Docker 开发;若想连接其他 Postgres 或 Electric(例如 Electric Cloud 托管实例),修改DATABASE_URLELECTRIC_URL即可。

安装依赖并启动

pnpm install

以后台方式启动后端服务(Postgres 与 Electric):

pnpm backend:up

应用数据库迁移:

pnpm migrate

启动开发服务器:

pnpm dev

打开https://localhost:5173即可访问应用。

环境变量详解

.env.example 定义了如下变量:

变量默认值说明
DATABASE_URLpostgresql://postgres:password@localhost:54321/electricPostgreSQL 连接串,需与 docker-compose.yaml 中的数据库配置一致
BETTER_AUTH_SECRET认证密钥,生产环境必填,建议至少 32 字符
ELECTRIC_URL注释状态托管 Electric 端点(如https://api.electric-sql.cloud);不设置时回退到本地http://localhost:30000
ELECTRIC_SOURCE_ID/ELECTRIC_SECRET注释状态Electric Cloud 的源凭证,由托管面板提供或通过npx @electric-sql/start自动申请

注意docker-compose.yaml中定义的默认端口:Postgres 对外映射54321:5432,Electric 对外映射30000:3000,启动 Postgres 时显式开启了wal_level=logical(逻辑复制是 Electric 读取变更的前提),并设置了ELECTRIC_INSECURE: true(该配置仅适用于本地开发,生产环境必须按官方安全指南关闭)。

认证是如何工作的:Shape Proxy 模式

如果你刚接触 Electric,有必要先理解一个关键差异:Electric 是持续的同步连接,而不是传统 REST API。传统 REST 的每个请求都单独鉴权;而 Electric 经 HTTP 流式推送数据,客户端与服务器之间维持长连接,这要求一套不同的授权思路。

该 Starter 采用Shape Proxy 模式

  1. 用户通过 Better Auth 认证(会话 Cookie);
  2. 服务端 API 路由在把请求代理给 Electric 之前校验会话;
  3. 行级过滤(WHERE子句)确保用户只能看到自己的数据;
  4. tRPC 变更在服务端二次校验权限。

架构图

┌─────────────────────────────────────────────────────────────┐ │ Client (Browser) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ TanStack DB │ │ Electric │ │ tRPC │ │ │ │ Collection │───▶│ Shape Client │ │ Client │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ └─────────│───────────────────│────────────────────│──────────┘ │ │ │ │ Session Cookie │ (automatic) │ │ ▼ ▼ ┌─────────│───────────────────────────────────────────────────┐ │ │ Server (TanStack Start) │ │ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ │ Shape Proxy Routes │ │ │ │ │ (/api/todos, /api/projects, etc) │ │ │ │ │ │ │ │ │ │ 1. Validate session │ │ │ │ │ 2. Add WHERE user_id = ? │ │ │ │ │ 3. Forward to Electric │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ │ │ ┌────────────────────│───────────────────────┐ │ │ │ │ tRPC Router ▼ │ │ │ └───▶│ - Validates session │ │ │ │ - Checks ownership before mutations │ │ │ │ - Returns transaction IDs for sync │ │ │ └────────────────────────────────────────────┘ │ │ │ │ └───────────────────────────────────│─────────────────────────┘ │ ┌────────────────┴────────────────┐ ▼ ▼ ┌──────────┐ ┌──────────┐ │ Electric │ │ Postgres │ │ Server │◀────────────────────▶│ Database │ └──────────┘ └──────────┘

Shape 代理路由的源码剖析

每个经 Electric 同步的表都对应一个充当"认证代理"的 API 路由。以 src/routes/api/todos.ts 为例,完整流程为:

const serve = async ({ request }: { request: Request }) => { // 1. 校验会话 const session = await auth.api.getSession({ headers: request.headers }) if (!session) { return new Response(JSON.stringify({ error: `Unauthorized` }), { status: 401, headers: { "content-type": `application/json` }, }) } // 2. 构造带行级过滤的 Electric URL const originUrl = prepareElectricUrl(request.url) originUrl.searchParams.set(`table`, `todos`) // 只同步用户有权限的行(参数化查询,防 SQL 注入) originUrl.searchParams.set(`where`, `$1 = ANY(user_ids)`) originUrl.searchParams.set(`params[1]`, session.user.id) // 3. 代理请求到 Electric return proxyElectricRequest(originUrl) } export const Route = createFileRoute(`/api/todos`)({ server: { handlers: { GET: serve } }, })

代理的两个核心辅助函数定义在 src/lib/electric-proxy.ts:

  • prepareElectricUrl(requestUrl):把入站请求 URL 重组为 Electric 的/v1/shape端点。它只从原始请求中拷贝 Electric 协议相关的查询参数(通过@electric-sql/client导出的ELECTRIC_PROTOCOL_QUERY_PARAMS白名单过滤),避免把无关参数透传;若配置了ELECTRIC_SOURCE_IDELECTRIC_SECRET,还会自动附加 Electric Cloud 的source_idsecret参数。
  • proxyElectricRequest(originUrl):对 Electric 发起fetch并把响应体流式返回,同时删除content-encodingcontent-length头(避免流式响应长度不可知导致的问题),并把vary: cookie加入响应头,确保按 Cookie 区分的代理响应不会被 HTTP 缓存层错误复用。

该模式带来三个关键保证:

  • Electric 永远不会收到未认证请求—— 代理先行校验;
  • 用户只能看到自己的数据——WHERE子句在数据库层面完成过滤;
  • 会话 Cookie 自动生效—— 客户端无需任何特殊配置(Shape Client 与代理路由同源,Cookie 自动携带)。

不同表的过滤条件各不相同,例如 src/routes/api/projects.ts 用的是"本人拥有或共享给我的项目":

originUrl.searchParams.set( `where`, `owner_id = $1 OR $1 = ANY(shared_user_ids)` ) originUrl.searchParams.set(`params[1]`, session.user.id)

这里有两个值得注意的细节:一是共享语义通过text[]数组列shared_user_ids表达;二是SQL 过滤条件一律使用$1参数占位符配合params[1]传值,从源头规避 SQL 注入风险。

变更授权:tRPC 层再做一次校验

Electric 负责读,写则走 tRPC,且带额外的服务端授权。以 src/lib/trpc/todos.ts 中的delete为例:

delete: authedProcedure .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { const result = await ctx.db.transaction(async (tx) => { const txid = await generateTxId(tx) const [deletedItem] = await tx .delete(todosTable) .where( and( eq(todosTable.id, input.id), arrayContains(todosTable.user_ids, [ctx.session.user.id]) // 所有权校验 ) ) .returning() if (!deletedItem) { throw new TRPCError({ code: `NOT_FOUND`, message: `Todo not found or you do not have permission to delete it`, }) } return { item: deletedItem, txid } }) return result }),

关键点:

  • authedProcedure是 src/lib/trpc.ts 中定义的标准中间件:上下文里没有用户即抛出UNAUTHORIZED。所有变更过程都挂在这个 procedure 下。
  • 所有权检查内联在 SQL 中:删除/更新语句的WHERE同时包含主键与user_ids数组包含当前用户两个条件,查不到记录就抛NOT_FOUND,从查询层面杜绝越权。
  • generateTxId(tx):在每个变更事务里调用SELECT pg_current_xact_id()::xid::text取出 PostgreSQL 当前事务 ID(去掉 epoch 的 32 位原始值,与逻辑复制流中 Electric 暴露的事务 ID 一致),随变更结果返回给客户端,客户端据此把本地乐观变更与后端写入收敛对齐。

projects路由遵循同一套模式,但所有权依据是owner_id必须等于当前会话用户(见 src/lib/trpc/projects.ts)。

给新表加认证的三个要点

新增一张可同步表时,认证相关要做三件事(完整流程见下文"添加一张新表"):

  1. schema 中包含用户引用(如user_id列,todos 表还用了user_ids数组列支持共享);
  2. 创建 shape 代理路由,校验会话并按用户过滤;
  3. tRPC 变更中加入所有权检查

命名约定:数据库统一使用 snake_case

Starter 对所有数据库列名统一采用snake_case,这是 PostgreSQL 的惯例,也是对 Electric 一致性至关重要的一点:

  1. PostgreSQL 惯例:snake_case 是 Postgres 列命名标准;
  2. Electric 兼容性:Electric 按数据库中的原始列名同步数据;
  3. 一致性:从数据库到前端保持同名,减少心智负担。

在 src/db/schema.ts 中定义 Drizzle schema 时,列名使用 snake_case:

export const todosTable = pgTable(`todos`, { id: integer().primaryKey().generatedAlwaysAsIdentity(), text: varchar({ length: 500 }).notNull(), completed: boolean().notNull().default(false), created_at: timestamp({ withTimezone: true }).notNull().defaultNow(), // snake_case user_id: text(`user_id`).notNull(), // snake_case project_id: integer(`project_id`).notNull(), // snake_case user_ids: text(`user_ids`).array().notNull().default([]), // 共享用户数组 })

TypeScript 类型与 Zod schema 同步使用 snake_case:

type Todo = { id: number text: string completed: boolean created_at: Date // matches database user_id: string // matches database project_id: number // matches database }

Schema 定义后,借助drizzle-zod的工厂函数自动派生 select/insert/update 三套 Zod schema(见 schema.ts),tRPC 的输入校验与 TanStack DB Collection 的行解析都复用它们。数据库连接层在 src/db/connection.ts 中也显式声明casing: 'snake_case',保证 Drizzle 与 Postgres 命名一致。

想用 camelCase 怎么办

若偏好 TypeScript 侧使用 camelCase,@electric-sql/client提供columnMapper选项自动转换列名:

import { ShapeStream, snakeCamelMapper } from "@electric-sql/client" const stream = new ShapeStream<Todo>({ url: "http://localhost:3000/v1/shape", params: { table: "todos" }, columnMapper: snakeCamelMapper(), // created_at → createdAt })

该 mapper 同样处理 where 子句:where: "userId = $1"发往 Electric 时会自动变成user_id = $1

开发实战:添加一张新表(完整七步)

以新增一张 "categories" 表为例,从建表到 UI 查询一共七步。

1. 定义 Drizzle schema

在 src/db/schema.ts 中添加表并派生 Zod schema:

export const categoriesTable = pgTable(`categories`, { id: integer().primaryKey().generatedAlwaysAsIdentity(), name: varchar({ length: 255 }).notNull(), color: varchar({ length: 7 }), // hex color created_at: timestamp({ withTimezone: true }).notNull().defaultNow(), user_id: text(`user_id`) .notNull() .references(() => users.id, { onDelete: `cascade` }), }) // 派生 Zod schemas export const selectCategorySchema = createSelectSchema(categoriesTable) export const createCategorySchema = createInsertSchema(categoriesTable).omit({ created_at: true, }) export const updateCategorySchema = createUpdateSchema(categoriesTable)

2. 生成并应用迁移

# 生成迁移文件 pnpm migrate:generate # 应用迁移到数据库 pnpm migrate

两个命令分别对应 package.json 中的drizzle-kit generatedrizzle-kit migrate

3. 暴露 Electric shape 代理路由

创建 src/routes/api/categories.ts:

import { createFileRoute } from "@tanstack/react-router" import { auth } from "@/lib/auth" import { prepareElectricUrl, proxyElectricRequest } from "@/lib/electric-proxy" const serve = async ({ request }: { request: Request }) => { const session = await auth.api.getSession({ headers: request.headers }) if (!session) { return new Response(JSON.stringify({ error: `Unauthorized` }), { status: 401, headers: { "content-type": `application/json` }, }) } const originUrl = prepareElectricUrl(request.url) originUrl.searchParams.set(`table`, `categories`) // 只过滤用户自己的分类(参数化查询) originUrl.searchParams.set(`where`, `user_id = $1`) originUrl.searchParams.set(`params[1]`, session.user.id) return proxyElectricRequest(originUrl) } export const Route = createFileRoute(`/api/categories`)({ server: { handlers: { GET: serve } }, })

4. 添加 tRPC router

创建 src/lib/trpc/categories.ts:

import { router, authedProcedure, generateTxId } from "@/lib/trpc" import { z } from "zod" import { eq, and } from "drizzle-orm" import { categoriesTable, createCategorySchema, updateCategorySchema } from "@/db/schema" export const categoriesRouter = router({ create: authedProcedure .input(createCategorySchema) .mutation(async ({ ctx, input }) => { const result = await ctx.db.transaction(async (tx) => { const txid = await generateTxId(tx) const [newItem] = await tx .insert(categoriesTable) .values({ ...input, user_id: ctx.session.user.id }) .returning() return { item: newItem, txid } }) return result }), // update / delete 遵循同一模式:where 中带所有权条件,查不到抛 NOT_FOUND })

注意create里强制把user_id覆盖为ctx.session.user.id,防止客户端伪造归属。

5. 挂载 tRPC router

在 src/routes/api/trpc/$.ts 中注册:

import { categoriesRouter } from "./trpc/categories" export const appRouter = router({ // ... existing routers categories: categoriesRouter, })

这个文件同时是 tRPC 的 HTTP 适配器:fetchRequestHandler接收 GET/POST,createContext里把db与经auth.api.getSession解析的session注入每个过程。

6. 添加 TanStack DB Collection

在 src/lib/collections.ts 中注册:

export const categoriesCollection = createCollection( electricCollectionOptions({ id: `categories`, shapeOptions: { url: `/api/categories`, parser: { timestamptz: (date: string) => new Date(date), }, }, schema: selectCategorySchema, getKey: (item) => item.id, onInsert: async ({ transaction }) => { const { modified: newCategory } = transaction.mutations[0] const result = await trpc.categories.create.mutate({ name: newCategory.name, color: newCategory.color, }) return { txid: result.txid } }, // onUpdate、onDelete 按需补充 }) )

shapeOptions.url指向第 3 步的代理端点(同源,Cookie 自动携带),parser.timestamptz把字符串时间戳解析成Date对象,onInsert把本地插入转成 tRPC 变更并回传 txid。

7. 在路由中使用 Collection

在路由 loader 中预加载、在组件里用useLiveQuery消费:

// 路由 loader 中 export const Route = createFileRoute(`/my-route`)({ loader: async () => { await Promise.all([categoriesCollection.preload()]) }, }) // 组件中 const { data: categories } = useLiveQuery((q) => q.from({ categoriesCollection }).orderBy(/* ... */) )

至此,新表已完整接入 Electric 同步、tRPC 变更与 TanStack DB 查询。

TanStack DB 与 Electric 的深度集成

TanStack DB 为实时同步提供稳健支持:实时查询、跨集合 join、本地写、无过期数据、亚毫秒级跨集合查询。而 Electric 负责解决同步中最难的部分:部分复制(shape)、扇出(fan-out)与数据投递。二者结合后:

  • 基于 TypeScript 实现的differential dataflow查询引擎,复杂 join 与聚合的实时查询也能增量更新、亚毫秒级响应;
  • 细粒度响应式,最小化组件重渲染;
  • 健壮的事务原语,乐观变更带同步与生命周期支持;
  • 数据规范化,保持后端简单。

核心概念

  • Collections:有类型的对象集合,可镜像后端表,也可承载过滤视图(如pendingTodosdecemberNewTodos)。Collection 就是可按需加载的普通 JavaScript 数据。
  • Live Queries:针对(跨)集合响应式执行查询,支持 join、过滤与聚合;由 differential dataflow 驱动,查询结果增量更新而无需重跑整条查询。
  • 事务性乐观变更:跨集合批量、分阶段地应用本地改动,立即呈现乐观结果,再与后端同步,自动回滚并管理乐观状态。

与 Electric 结合的使用方式

collections.ts 中真实的 todo collection 配置:

export const todoCollection = createCollection( electricCollectionOptions<Todo>({ id: `todos`, shapeOptions: { url: `/api/todos`, parser: { timestamptz: (date: string) => new Date(date), }, }, schema: selectTodoSchema, getKey: (item) => item.id, onInsert: async ({ transaction }) => { const { modified: newTodo } = transaction.mutations[0] const result = await trpc.todos.create.mutate({ user_id: newTodo.user_id, text: newTodo.text, completed: newTodo.completed, project_id: newTodo.project_id, user_ids: newTodo.user_ids, }) return { txid: result.txid } }, onUpdate: async ({ transaction }) => { const { modified: updatedTodo } = transaction.mutations[0] const result = await trpc.todos.update.mutate({ id: updatedTodo.id, data: { text: updatedTodo.text, completed: updatedTodo.completed }, }) return { txid: result.txid } }, onDelete: async ({ transaction }) => { const { original: deletedTodo } = transaction.mutations[0] const result = await trpc.todos.delete.mutate({ id: deletedTodo.id }) return { txid: result.txid } }, }) )

写入时先产生本地乐观状态,再经 collection 回调同步到后端:

const AddTodo = () => { return ( <Button onClick={() => todoCollection.insert({ id: crypto.randomUUID(), text: "Make app faster", completed: false, }) } /> ) }

注意真实代码中onInsert会把user_idproject_iduser_ids一并传给 tRPC,这正是"写路径在服务端再次确认归属"的一环。

跨集合 join 的实时查询

src/routes/_authenticated.tsx 展示了"项目侧边栏"与"登录态"的组合用法,而跨集合 join 的标准写法是:

import { useLiveQuery, eq } from "@tanstack/react-db" const Todos = () => { const { data: todos } = useLiveQuery((q) => q .from({ todo: todoCollection }) .join({ list: listCollection }, ({ list, todo }) => eq(list.id, todo.list_id) ) .where(({ list }) => eq(list.active, true)) .select(({ list, todo }) => ({ id: todo.id, status: todo.status, text: todo.text, list_name: list.name, })) ) return ( <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.text} - {todo.list_name}</li> ))} </ul> ) }

tRPC 客户端与 API 路由总览

src/lib/trpc-client.ts 使用httpBatchLink指向同源的/api/trpc,并把document.cookie放进请求头,实现全链路类型安全:

export const trpc = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: `/api/trpc`, async headers() { return { cookie: typeof document !== `undefined` ? document.cookie : `` } }, }), ], })

Starter 提供的 API 路由:

  • /api/trpc/*—— tRPC 变更,全程类型安全;
  • /api/auth/*—— Better Auth 认证;
  • /api/projects/api/todos/api/users—— Electric 同步 shape(读)。

核心架构规则

遵循这三条规则才能发挥 Starter 的最大价值:

  1. 读走 Electric:用useLiveQuery+ collections,而不是 tRPC query。直接 tRPC 读会绕开实时同步与乐观更新。
  2. 写走 collection 操作:调用collection.insert(),而不是直接调trpc.create.mutate()。collection 操作是乐观的 —— 先更新 UI,后台再同步。
  3. 在路由 loader 中预加载 collection:组件渲染前数据就绪,避免加载闪烁。

关于 Caddy:本地 HTTP/2 的关键

为什么需要它

Electric 的 shape 投递显著受益于 HTTP/2 多路复用。没有 HTTP/2 时,每个 shape 订阅都会新建一条 HTTP/1.1 连接,而浏览器限制每个域名最多 6 条并发连接 —— 这会成为瓶颈,让 shape 显得很慢。

Caddy 提供带自动 HTTPS 的 HTTP/2,带来:

  • 更快的 shape 加载—— 多个 shape 在单条连接上并发加载;
  • 更佳的开发体验—— 无连接数限制或人为延迟;
  • 贴近生产的性能—— 本地开发即与生产环境一致的 HTTP/2 行为。

Vite 开发服务器只跑 HTTP/1.1,所以由 Caddy 作为反向代理完成连接升级。

安装与信任证书

caddy trust

信任本地根证书后,浏览器访问本地 HTTPS 站点时才不会报 SSL 警告/错误。

它是如何自动工作的

从 vite.config.ts 可以看到一个自定义 Vite 插件caddyPlugin(),其实现位于 src/vite-plugin-caddy.ts:

  • 启动pnpm dev时,插件在 Vite 服务器就绪后检测caddy二进制是否可用(caddy --version),失败则提示安装并退出;
  • 自动生成项目根目录下的Caddyfile,内容为把https://localhost:5173反向代理到 Vite 端口并开启 gzip 编码;
  • caddy run --config Caddyfile拉起 Caddy 进程,并在 Vite 退出时通过 SIGTERM/SIGKILL 清理;
  • 重写 Vite 的printUrls,提示应用运行在https://localhost:5173/且经由 Caddy。

因此应用地址是https://localhost:5173(README 中描述为https://<project-name>.localhost的变体形态,取决于 Caddy 配置);直接访问http://localhost:5173依然可用,但 Electric shape 会慢一些。

Caddy 故障排查

  1. 手动测试 Caddycaddy start
  2. 检查证书信任caddy trust;之后移除用caddy untrust
  3. 确认 Caddyfile 已生成:运行pnpm dev后查看项目根目录是否有Caddyfile
  4. 停止冲突的 Caddy 实例caddy stop
  5. 检查端口占用:Caddy 需要 80 与 443 端口可用

故障排查速查表

常见问题

问题症状解决方案
Docker 未运行docker compose ps无输出启动 Docker Desktop/daemon
Caddy 证书未信任浏览器出现 SSL 警告执行caddy trust
端口冲突Postgres(54321)或 Electric(30000)被占用停止冲突服务,或在 docker-compose.yaml 中修改端口
缺少 .env数据库连接错误复制.env.example.env
Caddy 启动失败Caddy exited with code 1手动执行caddy start查看错误

调试命令

# 查看 Docker 服务状态 docker compose ps # 查看 Electric 与 Postgres 日志 docker compose logs -f electric postgres # 测试数据库连通性 psql $DATABASE_URL -c "SELECT 1" # 检查 Caddy 状态 caddy start

项目还提供了pnpm backend:down(停止后端)与pnpm backend:clear(停止并删除数据卷,用于重置本地数据)两个脚本。

构建与生产部署

构建生产版本:

pnpm build

生产部署检查清单

必填环境变量:

# 认证 - 生产必填 BETTER_AUTH_SECRET=your-secret-key-here # Electric Cloud(若使用托管 Electric) ELECTRIC_SOURCE_ID=your-source-id ELECTRIC_SOURCE_SECRET=your-source-secret # 数据库(按生产库调整) DATABASE_URL=postgresql://user:pass@your-prod-db:5432/dbname

认证设置(重要):当前开发模式下允许任意邮箱/密码组合登录,这在生产环境会自动禁用(见 src/lib/auth.ts 中disableSignUp: process.env.NODE_ENV === 'production'与密码最小长度 8 位的配置),但你需要:

  1. src/lib/auth.ts中配置正式的认证提供商(Google、GitHub 等);
  2. 如仍使用邮箱/密码登录,移除或加固仅用于开发的认证模式;
  3. 为生产域名复查trustedOrigins(当前配置包含https://tanstack-start-db-electric-starter.localhost、局域网 IP 与http://localhost:5173回退项,并借助tanstackStartCookies()插件适配 TanStack Start 的 Cookie 机制)。

基础设施变更:

  • HTTPS 与安全 Cookie:由部署平台负责 HTTPS 终止;
  • 数据库:使用托管 PostgreSQL 服务(而非 Docker 容器);
  • 环境:设置NODE_ENV=production

安全考量:

  • 生成强BETTER_AUTH_SECRET(至少 32 字符);
  • 确保数据库凭据妥善保管;
  • 若跨域名提供服务,复查 CORS 设置;
  • 确认开发模式认证已禁用。

项目辅助信息

  • AI 协作:Starter 内置AGENTS.md,若使用其他 AI 编程工具,可能需要把它复制/移动到对应位置(如.cursor/rules)。
  • 样式:项目使用 Tailwind CSS(v4,经@tailwindcss/vite插件接入)。
  • 路由:基于 TanStack Router 的文件式路由,路由文件位于src/routes,布局在src/routes/__root.tsx,用<Outlet />渲染子路由内容,登录态布局见 src/routes/_authenticated.tsx(beforeLoad中缓存/校验会话、未登录重定向到/login)。
  • 数据预取:除 TanStack DB 外,也可用路由 loader 预取远程数据;ssr: false的认证路由会在客户端先行加载会话后再渲染。

小结

这个 Starter 的价值在于把"读经 Electric 实时同步、写经 tRPC 类型安全变更、前端经 TanStack DB 乐观更新、认证经 Better Auth + Shape Proxy 统一收敛"的完整闭环固化成了可复用的模板。掌握 Shape Proxy 模式的四层保证(会话校验、行级过滤、服务端重校验、txid 对齐)、snake_case 命名约定与七步加表流程后,你可以把任意 Postgres 表快速接入这套实时、安全、本地优先的应用架构。

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询