Supabase 配置
创建桌面应用所需的 Supabase 后端、数据库结构、Storage 桶和 Edge Functions。
Tauri 模板使用 Supabase 提供邮箱认证、资料、待办事项示例、头像存储以及 Creem 权益的读取模型。本指南会将这些功能连接到你自己的项目。
仓库只提交了计费 subscriptions 与 payments 的迁移;应用实际使用的
profiles、todos 和私有 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-checkout | true | 仅已登录用户可开始购买。 |
customer-portal | true | 仅已登录用户可获取客户门户链接。 |
activate-license | true | 仅已登录用户可激活许可证。 |
creem-webhook | false | Creem 不会携带 Supabase JWT;处理器会自行验证 Creem 签名。 |
应用数据库结构
先应用已提交的计费迁移:
supabase db push --project-ref <ref>它会创建 subscriptions、payments、枚举、索引、更新时间触发器与 RLS。客户端
只能读取自己的计费行;Creem Edge Functions 使用仅服务端持有的 service-role
凭据写入,因此桌面客户端无法自行授予套餐。
然后打开 Supabase Dashboard 的 SQL Editor,执行下列 SQL。字段与
src/services/profile.ts、todos.ts、avatar.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生成后的文件应更小;核心表应为 profiles、todos、subscriptions、payments。
检查 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显示四个函数。
