数据层

类型化 Supabase 服务、TanStack Query,以及可替换后端的接入点。

模板将后端访问保持得很小:类型化 Supabase 调用放在 service 模块中,TanStack Query 管理服务端缓存,zustand store 协调跨路由的会话与权益状态。日后替换为自己的 API 时, service 就是唯一的切换点。

分层

位置职责
类型化 clientsrc/lib/supabase/client.ts使用钥匙串认证 adapter 的 createClient<Database>()
类型映射src/lib/supabase/database.types.tsTablesTablesInsertTablesUpdate 导入类型。
Query clientsrc/lib/query/client.ts全局一个 client:重试一次、staleTime 为 30 秒、窗口聚焦不重取。
Servicessrc/services/*.ts纯 Supabase 表、Storage 与函数调用;不导入组件。
Auth storesrc/stores/auth-store.ts会话、当前用户与资料。
Subscription storesrc/stores/subscription-store.ts账户绑定的权益状态。

组件不应散落 supabase.from(...) 调用。每个功能将数据访问集中在 service,再从 query、 mutation 或小型 store action 调用,便于测试、替换与保持类型边界。

Todos:完整示例

Showcase 的 Todos demo 展示一个完整的所有者范围功能:

部分实现
Servicesrc/services/todos.ts 列表、新建、切换和删除 public.todos
稳定缓存 keytodosQueryKey(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.tsmaybeSingle() 读取当前用户唯一的 subscriptions 行。
Edge FunctionsCreem webhook 与许可证流程用仅服务端凭据写入计费记录。
RLS客户端仅可读取自己的 subscriptionspayments,没有客户端写策略。
Storesubscription-store.ts 推导 disabledfreeactiveexpiredunknown

unknown 很重要:网络或 RLS 失败不能默默当作 free(隐藏已付费用户的权益),也不能 当作 active(无凭据解锁功能)。计费关闭时 store 为 disabled,完全不会读取后端。 权益 gate 请见 计费,写入端请见 Creem 配置

端到端添加功能

在 Supabase 创建表、索引与所有者范围 RLS。始终把桌面 webview 视为不可信的公开客户端。
从你的项目重新生成 src/lib/supabase/database.types.ts,并将 SQL 与生成类型一起提交。
src/services/ 新建专注的模块,集中表/Storage 调用、选择字段与错误处理。
在路由中使用稳定的 TanStack Query key 和 query/mutation 或 store action;变更后使对应用户缓存失效或更新缓存。
添加路由、导航、翻译与未登录状态。若外部深链应打开该页面,也把精确路径加入 NAVIGATION_ROUTES

离线与网络行为

src/hooks/use-online-status.ts 经由 useSyncExternalStore 使用 webview 的 online/ offline 事件,shell 用它显示离线提示。它反映的是系统网络接口状态,不是 Supabase 一定可达的证明。Query client 对失败请求只重试一次,且不会将缓存持久化到磁盘。

对账户或付费状态,请明确建模读取失败。订阅 store 的 unknown 是良好默认值:显示中性的 重试状态,而不是猜测用户是否有权限。

相关页面

On this page