数据层
类型化 Supabase 服务、TanStack Query,以及可替换后端的接入点。
模板将后端访问保持得很小:类型化 Supabase 调用放在 service 模块中,TanStack Query 管理服务端缓存,zustand store 协调跨路由的会话与权益状态。日后替换为自己的 API 时, service 就是唯一的切换点。
分层
| 层 | 位置 | 职责 |
|---|---|---|
| 类型化 client | src/lib/supabase/client.ts | 使用钥匙串认证 adapter 的 createClient<Database>()。 |
| 类型映射 | src/lib/supabase/database.types.ts | 以 Tables、TablesInsert、TablesUpdate 导入类型。 |
| Query client | src/lib/query/client.ts | 全局一个 client:重试一次、staleTime 为 30 秒、窗口聚焦不重取。 |
| Services | src/services/*.ts | 纯 Supabase 表、Storage 与函数调用;不导入组件。 |
| Auth store | src/stores/auth-store.ts | 会话、当前用户与资料。 |
| Subscription store | src/stores/subscription-store.ts | 账户绑定的权益状态。 |
组件不应散落 supabase.from(...) 调用。每个功能将数据访问集中在 service,再从 query、
mutation 或小型 store action 调用,便于测试、替换与保持类型边界。
Todos:完整示例
Showcase 的 Todos demo 展示一个完整的所有者范围功能:
| 部分 | 实现 |
|---|---|
| Service | src/services/todos.ts 列表、新建、切换和删除 public.todos。 |
| 稳定缓存 key | todosQueryKey(userId) 返回 ['todos', userId]。 |
| 排序 | 先按 sort_order 升序,再按 created_at 降序。 |
| 所有权 | 客户端插入 user_id,RLS 确认其等于 auth.uid()。 |
| 错误 | service 抛出 Supabase 错误,UI 可显示本地化反馈。 |
真正阻止用户读取或修改他人待办事项的是 RLS,不是 UI 中隐藏的 ID。请以 Supabase 配置 的 SQL 作为新增数据表的范式。
资料与头像服务
profile.ts 按用户 ID 读取一行资料并更新可编辑字段。avatar.ts 验证字节、上传到
私有 avatars 桶,再把 Storage 路径写入 profiles.avatar_url。展示时将路径转换成
七天有效的签名 URL;删除时先删除对象,再清空资料字段。
因此数据库不需要保存公开可读的头像 URL,Storage 策略也很直接:每个用户只能操作
自己的 <user-id>/ 文件夹。
订阅读取模型
计费将 Supabase 作为所有权的真实来源:
| 部分 | 规则 |
|---|---|
src/services/subscription.ts | 用 maybeSingle() 读取当前用户唯一的 subscriptions 行。 |
| Edge Functions | Creem webhook 与许可证流程用仅服务端凭据写入计费记录。 |
| RLS | 客户端仅可读取自己的 subscriptions 与 payments,没有客户端写策略。 |
| Store | subscription-store.ts 推导 disabled、free、active、expired 或 unknown。 |
unknown 很重要:网络或 RLS 失败不能默默当作 free(隐藏已付费用户的权益),也不能
当作 active(无凭据解锁功能)。计费关闭时 store 为 disabled,完全不会读取后端。
权益 gate 请见 计费,写入端请见
Creem 配置。
端到端添加功能
src/lib/supabase/database.types.ts,并将 SQL 与生成类型一起提交。src/services/ 新建专注的模块,集中表/Storage 调用、选择字段与错误处理。NAVIGATION_ROUTES。离线与网络行为
src/hooks/use-online-status.ts 经由 useSyncExternalStore 使用 webview 的 online/
offline 事件,shell 用它显示离线提示。它反映的是系统网络接口状态,不是
Supabase 一定可达的证明。Query client 对失败请求只重试一次,且不会将缓存持久化到磁盘。
对账户或付费状态,请明确建模读取失败。订阅 store 的 unknown 是良好默认值:显示中性的
重试状态,而不是猜测用户是否有权限。
