
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类型安全变更与 ElectricPostgres 实时同步引擎组合成一个认证安全、实时同步、乐观更新的现代 Web 应用。读完本文你将掌握 Shape Proxy 认证模式的完整实现原理、从零新增一张可同步表的七个标准步骤以及本地开发中 Caddy 提供 HTTP/2 加速 shape 交付的完整机制。技术栈与核心架构总览该 Starter 解决的核心问题是把三套互补的技术拼接成一个自洽的实时应用骨架技术职责仓库中的体现TanStack Start全栈 React 框架文件式路由 服务端路由处理器路由定义TanStack DB客户端嵌入式数据库实时查询、跨集合 join、本地写collections.tsElectricPostgres 同步引擎以 shape表的过滤视图形式经 HTTP 向客户端增量推送数据docker-compose.yamlBetter Auth会话认证Cookie负责登录、鉴权auth.tstRPC类型安全的写路径变更服务端二次校验权限trpc.tsDrizzle ORM数据库建模与迁移Zod schema 自动派生schema.ts数据流向可以概括为读走 Electric、写走 tRPC读路径浏览器中的 TanStack DB 通过 Electric Shape Client 订阅 shape指向/api/xxx代理端点服务端代理校验会话后转发给 Electric 服务Electric 从 Postgres 拉取逻辑复制变更后推送给客户端。写路径collection.insert()先在本地乐观更新随后调用 tRPC 变更服务端在事务中再次校验所有权并写入数据库最后通过返回的事务 IDtxid驱动本地乐观状态与后端同步收敛。快速开始前置条件运行本项目需要三样东西Docker用于运行 docker-compose.yaml 中定义的 Postgres 与 Electric 服务。Caddy提供本地 HTTPS从而启用 HTTP/2 多路复用。安装后执行caddy trust可能需要 sudo信任其本地根证书。Node 与 pnpm示例仓库的package.json声明engines要求 Node20.19.0 || 22.12.0。为什么需要 CaddyElectric 的 shape 交付从HTTP/2 多路复用中显著获益HTTP/2 允许同一个连接上并发加载多个 shape而 HTTP/1.1 下浏览器对同一域名只允许 6 个并发连接每个 shape 订阅占用一个连接时会形成瓶颈表现为 shape 加载缓慢。HTTP/2 要求 HTTPS因此本地开发需要 Caddy 承担反向代理。Vite 开发服务器本身只跑 HTTP/1.1Caddy 负责升级连接详见下文关于 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 .envTip.env中的值可按需编辑。默认值面向本地 Docker 开发若想连接其他 Postgres 或 Electric例如 Electric Cloud 托管实例修改DATABASE_URL与ELECTRIC_URL即可。安装依赖并启动pnpm install以后台方式启动后端服务Postgres 与 Electricpnpm backend:up应用数据库迁移pnpm migrate启动开发服务器pnpm dev打开https://localhost:5173即可访问应用。环境变量详解.env.example 定义了如下变量变量默认值说明DATABASE_URLpostgresql://postgres:passwordlocalhost:54321/electricPostgreSQL 连接串需与 docker-compose.yaml 中的数据库配置一致BETTER_AUTH_SECRET空认证密钥生产环境必填建议至少 32 字符ELECTRIC_URL注释状态托管 Electric 端点如https://api.electric-sql.cloud不设置时回退到本地http://localhost:30000ELECTRIC_SOURCE_ID/ELECTRIC_SECRET注释状态Electric Cloud 的源凭证由托管面板提供或通过npx electric-sql/start自动申请注意docker-compose.yaml中定义的默认端口Postgres 对外映射54321:5432Electric 对外映射30000:3000启动 Postgres 时显式开启了wal_levellogical逻辑复制是 Electric 读取变更的前提并设置了ELECTRIC_INSECURE: true该配置仅适用于本地开发生产环境必须按官方安全指南关闭。认证是如何工作的Shape Proxy 模式如果你刚接触 Electric有必要先理解一个关键差异Electric 是持续的同步连接而不是传统 REST API。传统 REST 的每个请求都单独鉴权而 Electric 经 HTTP 流式推送数据客户端与服务器之间维持长连接这要求一套不同的授权思路。该 Starter 采用Shape Proxy 模式用户通过 Better Auth 认证会话 Cookie服务端 API 路由在把请求代理给 Electric 之前校验会话行级过滤WHERE子句确保用户只能看到自己的数据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.tsprepareElectricUrl(requestUrl)把入站请求 URL 重组为 Electric 的/v1/shape端点。它只从原始请求中拷贝 Electric 协议相关的查询参数通过electric-sql/client导出的ELECTRIC_PROTOCOL_QUERY_PARAMS白名单过滤避免把无关参数透传若配置了ELECTRIC_SOURCE_ID与ELECTRIC_SECRET还会自动附加 Electric Cloud 的source_id与secret参数。proxyElectricRequest(originUrl)对 Electric 发起fetch并把响应体流式返回同时删除content-encoding、content-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。给新表加认证的三个要点新增一张可同步表时认证相关要做三件事完整流程见下文添加一张新表schema 中包含用户引用如user_id列todos 表还用了user_ids数组列支持共享创建 shape 代理路由校验会话并按用户过滤tRPC 变更中加入所有权检查。命名约定数据库统一使用 snake_caseStarter 对所有数据库列名统一采用snake_case这是 PostgreSQL 的惯例也是对 Electric 一致性至关重要的一点PostgreSQL 惯例snake_case 是 Postgres 列命名标准Electric 兼容性Electric 按数据库中的原始列名同步数据一致性从数据库到前端保持同名减少心智负担。在 src/db/schema.ts 中定义 Drizzle schema 时列名使用 snake_caseexport 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_casetype 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.tstRPC 的输入校验与 TanStack DB Collection 的行解析都复用它们。数据库连接层在 src/db/connection.ts 中也显式声明casing: snake_case保证 Drizzle 与 Postgres 命名一致。想用 camelCase 怎么办若偏好 TypeScript 侧使用 camelCaseelectric-sql/client提供columnMapper选项自动转换列名import { ShapeStream, snakeCamelMapper } from electric-sql/client const stream new ShapeStreamTodo({ 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 schemaexport 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 generate与drizzle-kit migrate。3. 暴露 Electric shape 代理路由创建 src/routes/api/categories.tsimport { 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.tsimport { 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/POSTcreateContext里把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有类型的对象集合可镜像后端表也可承载过滤视图如pendingTodos、decemberNewTodos。Collection 就是可按需加载的普通 JavaScript 数据。Live Queries针对跨集合响应式执行查询支持 join、过滤与聚合由 differential dataflow 驱动查询结果增量更新而无需重跑整条查询。事务性乐观变更跨集合批量、分阶段地应用本地改动立即呈现乐观结果再与后端同步自动回滚并管理乐观状态。与 Electric 结合的使用方式collections.ts 中真实的 todo collection 配置export const todoCollection createCollection( electricCollectionOptionsTodo({ 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_id、project_id、user_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 createTRPCProxyClientAppRouter({ 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 的最大价值读走 Electric用useLiveQuery collections而不是 tRPC query。直接 tRPC 读会绕开实时同步与乐观更新。写走 collection 操作调用collection.insert()而不是直接调trpc.create.mutate()。collection 操作是乐观的 —— 先更新 UI后台再同步。在路由 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:5173README 中描述为https://project-name.localhost的变体形态取决于 Caddy 配置直接访问http://localhost:5173依然可用但 Electric shape 会慢一些。Caddy 故障排查手动测试 Caddycaddy start检查证书信任caddy trust之后移除用caddy untrust确认 Caddyfile 已生成运行pnpm dev后查看项目根目录是否有Caddyfile停止冲突的 Caddy 实例caddy stop检查端口占用Caddy 需要 80 与 443 端口可用故障排查速查表常见问题问题症状解决方案Docker 未运行docker compose ps无输出启动 Docker Desktop/daemonCaddy 证书未信任浏览器出现 SSL 警告执行caddy trust端口冲突Postgres54321或 Electric30000被占用停止冲突服务或在 docker-compose.yaml 中修改端口缺少 .env数据库连接错误复制.env.example为.envCaddy 启动失败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_SECRETyour-secret-key-here # Electric Cloud若使用托管 Electric ELECTRIC_SOURCE_IDyour-source-id ELECTRIC_SOURCE_SECRETyour-source-secret # 数据库按生产库调整 DATABASE_URLpostgresql://user:passyour-prod-db:5432/dbname认证设置重要当前开发模式下允许任意邮箱/密码组合登录这在生产环境会自动禁用见 src/lib/auth.ts 中disableSignUp: process.env.NODE_ENV production与密码最小长度 8 位的配置但你需要在src/lib/auth.ts中配置正式的认证提供商Google、GitHub 等如仍使用邮箱/密码登录移除或加固仅用于开发的认证模式为生产域名复查trustedOrigins当前配置包含https://tanstack-start-db-electric-starter.localhost、局域网 IP 与http://localhost:5173回退项并借助tanstackStartCookies()插件适配 TanStack Start 的 Cookie 机制。基础设施变更HTTPS 与安全 Cookie由部署平台负责 HTTPS 终止数据库使用托管 PostgreSQL 服务而非 Docker 容器环境设置NODE_ENVproduction。安全考量生成强BETTER_AUTH_SECRET至少 32 字符确保数据库凭据妥善保管若跨域名提供服务复查 CORS 设置确认开发模式认证已禁用。项目辅助信息AI 协作Starter 内置AGENTS.md若使用其他 AI 编程工具可能需要把它复制/移动到对应位置如.cursor/rules。样式项目使用 Tailwind CSSv4经tailwindcss/vite插件接入。路由基于 TanStack Router 的文件式路由路由文件位于src/routes布局在src/routes/__root.tsx用Outlet /渲染子路由内容登录态布局见 src/routes/_authenticated.tsxbeforeLoad中缓存/校验会话、未登录重定向到/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),仅供参考