Supabase 配置

创建桌面应用所需的 Supabase 后端、数据库结构、Storage 桶和 Edge Functions。

Tauri 模板使用 Supabase 提供邮箱认证、资料、待办事项示例、头像存储以及 Creem 权益的读取模型。本指南会将这些功能连接到你自己的项目。

仓库只提交了计费 subscriptionspayments 的迁移;应用实际使用的 profilestodos 和私有 avatars 桶没有对应 SQL。测试登录后的页面前, 请先执行本页 SQL。

创建并连接项目

Supabase 创建项目,将项目 URL 和 publishable key 写入根目录 .env

VITE_SUPABASE_URL=https://<ref>.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx

这两个值可以放进 webview 包中:它们只标识项目,数据访问由 RLS 控制。绝不 要将 service-role key 打包进桌面应用。

安装并登录 CLI,然后将 supabase/config.toml 里的示例 project_id 换成你的 项目 ref:

brew install supabase/tap/supabase # macOS
supabase login
supabase link --project-ref <ref>  # 可选,但很方便

config.toml 为每个 Edge Function 固定 JWT 验证设置:

函数verify_jwt原因
create-checkouttrue仅已登录用户可开始购买。
customer-portaltrue仅已登录用户可获取客户门户链接。
activate-licensetrue仅已登录用户可激活许可证。
creem-webhookfalseCreem 不会携带 Supabase JWT;处理器会自行验证 Creem 签名。

应用数据库结构

先应用已提交的计费迁移:

supabase db push --project-ref <ref>

它会创建 subscriptionspayments、枚举、索引、更新时间触发器与 RLS。客户端 只能读取自己的计费行;Creem Edge Functions 使用仅服务端持有的 service-role 凭据写入,因此桌面客户端无法自行授予套餐。

然后打开 Supabase Dashboard 的 SQL Editor,执行下列 SQL。字段与 src/services/profile.tstodos.tsavatar.ts 实际读写的字段一致。

create table if not exists public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  full_name text, bio text, phone text, location text, avatar_url text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.todos (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id) on delete cascade,
  title text not null, completed boolean not null default false,
  sort_order integer not null default 0,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists todos_user_id_idx on public.todos (user_id);
create index if not exists todos_user_sort_idx
  on public.todos (user_id, sort_order, created_at desc);

create or replace function public.set_updated_at()
returns trigger language plpgsql as $$
begin new.updated_at = now(); return new; end;
$$;

drop trigger if exists set_profiles_updated_at on public.profiles;
create trigger set_profiles_updated_at before update on public.profiles
  for each row execute function public.set_updated_at();
drop trigger if exists set_todos_updated_at on public.todos;
create trigger set_todos_updated_at before update on public.todos
  for each row execute function public.set_updated_at();

alter table public.profiles enable row level security;
alter table public.todos enable row level security;

drop policy if exists "profiles_select_own" on public.profiles;
create policy "profiles_select_own" on public.profiles
  for select to authenticated using ((select auth.uid()) = id);
drop policy if exists "profiles_update_own" on public.profiles;
create policy "profiles_update_own" on public.profiles
  for update to authenticated using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);

drop policy if exists "todos_select_own" on public.todos;
create policy "todos_select_own" on public.todos
  for select to authenticated using ((select auth.uid()) = user_id);
drop policy if exists "todos_insert_own" on public.todos;
create policy "todos_insert_own" on public.todos
  for insert to authenticated with check ((select auth.uid()) = user_id);
drop policy if exists "todos_update_own" on public.todos;
create policy "todos_update_own" on public.todos
  for update to authenticated using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);
drop policy if exists "todos_delete_own" on public.todos;
create policy "todos_delete_own" on public.todos
  for delete to authenticated using ((select auth.uid()) = user_id);

create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
  insert into public.profiles (id, full_name)
  values (new.id, new.raw_user_meta_data ->> 'full_name')
  on conflict (id) do nothing;
  return new;
end;
$$;

drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created after insert on auth.users
  for each row execute function public.handle_new_user();

insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values ('avatars', 'avatars', false, 5242880,
  array['image/png', 'image/jpeg', 'image/webp'])
on conflict (id) do update set public = excluded.public,
  file_size_limit = excluded.file_size_limit,
  allowed_mime_types = excluded.allowed_mime_types;

drop policy if exists "avatars_select_own" on storage.objects;
create policy "avatars_select_own" on storage.objects for select to authenticated using (
  bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text
);
drop policy if exists "avatars_insert_own" on storage.objects;
create policy "avatars_insert_own" on storage.objects for insert to authenticated with check (
  bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text
);
drop policy if exists "avatars_update_own" on storage.objects;
create policy "avatars_update_own" on storage.objects for update to authenticated using (
  bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text
) with check (
  bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text
);
drop policy if exists "avatars_delete_own" on storage.objects;
create policy "avatars_delete_own" on storage.objects for delete to authenticated using (
  bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text
);

该桶是私有的。对象路径为 <user-id>/avatar.png.jpg.webp;策略要求 路径的第一个目录必须等于当前用户 ID。应用展示头像时会创建有效期七天的签名 URL。

重新生成类型

src/lib/supabase/database.types.ts 目前是手工维护的,且混入了开发项目遗留的 chat、referral、sample-upload 类型。执行 SQL 后,请从自己的项目重新生成:

supabase gen types typescript --project-id <ref> \
  > src/lib/supabase/database.types.ts

生成后的文件应更小;核心表应为 profilestodossubscriptionspayments。 检查 diff 后提交,不要把无关表复制到你的产品 schema 中。

邮件与深链配置

内置注册和重设密码使用六码 OTP。请在 Supabase Authentication → Email Templates 的 Confirm signup 与 Reset password 模板中展示 {{ .Token }}。客户端设置了 detectSessionInUrl: false,因此当前流程不需要重定向 URL。

若你添加 magic link 或 OAuth,请在 Authentication → URL Configuration → Redirect URLs 注册要发布的 scheme,例如:

soar-tauri://auth/callback

若已改品牌,先执行 pnpm rebrand --scheme <your-scheme>。还必须在 src/lib/intents/routes.ts 添加回调路由;当前 allow-list 并不接受 /auth/callback,只在 Supabase 添加 URL 还不够。

部署 Edge Functions

计费函数不在应用包内。设置服务端密钥后,用辅助脚本部署:

supabase secrets set \
  CREEM_API_KEY=creem_live_xxx \
  CREEM_WEBHOOK_SECRET=whsec_xxx \
  CREEM_SERVER_IDX=0 \
  --project-ref <ref>

pnpm supabase:deploy <ref>
supabase functions list --project-ref <ref>

scripts/deploy-edge-functions.sh 会部署 supabase/functions/ 下所有非下划线 目录;_shared/ 会随使用它的函数打包,绝不能单独部署。产品、webhook 与结账配置 请见 Creem 配置

验证后端

  • 注册并输入六码验证码,确认回到首页。
  • 确认已创建对应的 profiles 行。
  • 在展示 → 待办中新建、完成并删除待办事项。
  • 在账户页上传头像,确认对象位于 avatars/<user-id>/
  • 确认 supabase functions list 显示四个函数。

相关页面

On this page