数据层

类型化的 Supabase 服务、TanStack Query 与订阅只读模型。

Electron 模板的数据访问保持小而可替换:类型化 Supabase 调用放在 app/services/,TanStack Query 负责 renderer cache 行为,zustand stores 保存跨 route 读取的 session 与 entitlement 状态。

层级文件作用
Supabase clientapp/lib/supabase/client.ts带安全桌面 session storage 的 createClient<Database>()
Database typesapp/lib/supabase/database.types.ts手工维护的 Supabase type map,供 service modules 使用。
Query clientapp/lib/query/client.ts全局 QueryClient,配置为 retry: 1staleTime: 30000refetchOnWindowFocus: false
Feature servicesapp/services/*.tsSupabase tables、Storage、Edge Functions、analytics 与 license activation 的纯模块。
Auth storeapp/stores/auth-store.tsSession、user、profile、auth-state subscription 与 sign-out。
Subscription storeapp/stores/subscription-store.ts启用 billing 后读取账号绑定的 entitlement 模型。

把 services 当作替换成你自己后端的边界。组件应通过 query、mutation 或 store action 调用 service,不要把 supabase.from(...) 分散在 UI 代码中。

Todos 示例

Todos 是 Example feature,用来展示端到端模式:

部分文件说明
Serviceapp/services/todos.tspublic.todos list、create、toggle、delete 行。
Query keyapp/lib/query/client.ts 中的 todosQueryKey(userId)按用户拆分 cache entries。
UIapp/routes/showcase/demos/TodosDemo.tsx要求已登录用户,使用 useQueryuseMutation,mutation 成功后更新 cache。
RLSSupabase Setup SQL用户只能 select、insert、update、delete user_id = auth.uid() 的行。

listTodos() 先按 sort_order 排序,再按最新 created_at 排序。Mutations 在 Supabase 报错时抛出异常,让 UI 显示本地化失败 toast。

Profile 与 avatar 示例

Profile 数据使用 app/services/profile.ts,avatar 上传使用 app/services/avatar.ts

  • getProfile(userId) 按 id select 一个 profiles 行。
  • updateProfile(userId, patch) 写入 profile 字段并更新 updated_at
  • uploadAvatar() 校验 PNG/JPEG/WebP 和 5 MB 上限,上传到 avatars/<user-id>/avatar.<ext>,然后把路径写入 profiles.avatar_url
  • getAvatarUrl() 把保存的路径转换成 7 天有效的 signed Storage URL。
  • removeAvatar() 删除 Storage object 并清空 avatar_url

这些 services 依赖 Supabase Setup 中的内联 schema 与 Storage policies。

Subscription read model

计费使用 read model,而不是信任本地购买状态:

部分文件规则
Table readapp/services/subscription.tsmaybeSingle() 读取已登录用户的 subscriptions 行。
Storeapp/stores/subscription-store.ts推导 freeactiveexpiredunknowndisabled
Product mappingshared/price-config.ts把 Creem product IDs 映射到 freeprolifetime
WritesSupabase Edge Functions客户端永远不写 subscriptionspayments;RLS 只授予 SELECT。

VITE_BILLING_ENABLED=false 时,subscription store 进入 disabled 并且不读取 Supabase。Session 或网络读取失败时,它使用 unknown,而不是 free,避免临时 错误错误降级已付费用户或解锁付费功能。

subscriptionspayments 的形状与移动模板中 RevenueCat webhook 使用的账号绑定 entitlement read model 一致,所以同时拥有多个模板的团队可以共享同一套后端概念。

添加一个功能

添加 SQL 和 RLS

创建 table、indexes、triggers 和 owner-scoped policies。对于桌面客户端写入, 始终假设 renderer 是公开的,让 RLS 负责授权。

更新 database types

重新生成或手工更新 app/lib/supabase/database.types.ts,让 services 可以导入 Tables<'your_table'>TablesInsert<'your_table'>TablesUpdate<'your_table'>

创建 service module

把 Supabase table、Storage 或 Edge Function 调用放到 app/services/。在这里规范化 错误,让组件代码专注 UI 状态。

添加 query 与 mutation 用法

共享 key 有价值时,在 app/lib/query/client.ts 添加稳定 query keys,然后从 useQueryuseMutation 或小型 store action 中调用 service。

渲染 route

添加 route、sidebar 或 command palette 入口、i18n strings 和 signed-out state。如果 功能需要 main process 能力,通过 Conveyor 暴露,不要在 renderer 里导入 Electron API。

Offline 与 network 行为

当前 query client 很简单:一次重试、30 秒 freshness,并且不在 window focus 时自动 refetch。它不会把 TanStack Query cache 持久化到磁盘。

app/hooks/use-online-status.tsuseSyncExternalStore 包装浏览器 onlineoffline 事件。它反映的是 OS 网络接口,不保证 Supabase 可达,更适合用作 提示信号。

对关键付费或账号数据,遵循 subscription store 的模式:把传输失败表示为 unknown, 这样 UI 能保持中性状态,而不是假装用户是 free 或 entitled。

相关页面

On this page