Supabase 配置
创建项目、应用数据库结构并部署 Edge Functions。
Supabase 支撑 Electron 模板里的邮箱认证、profile 数据、avatar 存储、todos 示例,以及按账号绑定的计费权益读模型。应用在 renderer 中使用 Supabase publishable key,但真正的授权必须由 Postgres RLS 和 Storage policies 保证。
当前仓库只提交了计费 migration。应用还会查询 profiles、todos 和私有
avatars bucket,所以本页提供了让后端可运行所需的补充 SQL。
创建项目
创建 Supabase 项目
在 Supabase dashboard 创建项目,然后从 Project Settings -> API 复制项目
URL 和 publishable key 到 soar-electron/.env:
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-anon-key这些值是公开客户端配置。不要把 service-role key 放进桌面应用 .env。
安装并关联 CLI
brew install supabase/tap/supabase
supabase login
supabase link --project-ref <ref>supabase/config.toml 保存 project ref 和 Edge Function JWT 设置。如果不使用
supabase link,就把 project_id 改成你的 ref。
应用计费 migration
supabase db push --project-ref <ref>这会应用 supabase/migrations/20260614120000_billing_entitlements.sql。
该 migration 创建 subscriptions 和 payments,模型是 service-role-write、
client-read:已登录客户端只能 select 自己的行,不能 insert、update 或 delete
计费记录。Edge Functions 使用 service role 写入权益行。
添加应用 schema
把下面的 SQL 粘贴到 Supabase SQL editor。它创建
app/services/profile.ts、app/services/todos.ts 和
app/services/avatar.ts 所需的表与 Storage policies。
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()
);
alter table public.profiles enable row level security;
drop policy if exists "Users can view own profile" on public.profiles;
create policy "Users can view own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = id);
drop policy if exists "Users can insert own profile" on public.profiles;
create policy "Users can insert own profile"
on public.profiles for insert
to authenticated
with check ((select auth.uid()) = id);
drop policy if exists "Users can update own profile" on public.profiles;
create policy "Users can update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = 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();
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.todos enable row level security;
drop policy if exists "Users can view their todos" on public.todos;
create policy "Users can view their todos"
on public.todos for select
to authenticated
using ((select auth.uid()) = user_id);
drop policy if exists "Users can insert their todos" on public.todos;
create policy "Users can insert their todos"
on public.todos for insert
to authenticated
with check ((select auth.uid()) = user_id);
drop policy if exists "Users can update their todos" on public.todos;
create policy "Users can update their todos"
on public.todos for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
drop policy if exists "Users can delete their todos" on public.todos;
create policy "Users can delete their todos"
on public.todos for delete
to authenticated
using ((select auth.uid()) = user_id);
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;
do $$
begin
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can read own avatar objects'
) then
execute $policy$
create policy "Users can read own avatar objects"
on storage.objects for select
to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can upload own avatar objects'
) then
execute $policy$
create policy "Users can upload own avatar objects"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can update own avatar objects'
) then
execute $policy$
create policy "Users can update own avatar objects"
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
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can delete own avatar objects'
) then
execute $policy$
create policy "Users can delete own avatar objects"
on storage.objects for delete
to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
end $$;数据库类型
app/lib/supabase/database.types.ts 目前在这个模板中是手工维护的。应用或修改
schema 后,可以仔细手改该文件,也可以重新生成:
npx supabase gen types typescript --project-id <ref> > app/lib/supabase/database.types.ts提交前检查 diff。当前文件包含一些 Electron 已提交 migration 未创建的 sample tables,所以重新生成可能会移除你已经删除或仍准备迁移的类型。
Auth URLs
当前邮箱流程使用应用内输入六位 OTP:
- 注册验证调用
supabase.auth.verifyOtp({ type: 'signup' })。 - 密码重置调用
supabase.auth.verifyOtp({ type: 'recovery' })。 app/renderer.tsx为 Supabase client 设置了detectSessionInUrl: false。
也就是说,当前代码路径不消费 Supabase auth callback deep link。如果之后把 Supabase 邮件模板改成链接式确认或接入 OAuth,仍应允许应用 scheme:
soar-electron://运行 pnpm rebrand --scheme 后使用改名后的 scheme。当前 deep-link route
allow-list 接受 soar-electron://settings、soar-electron://account、
soar-electron://billing 等 shell routes;不包含 /login 或
/reset-password。
Edge Functions
使用 helper script 部署 supabase/functions 下的计费函数:
pnpm supabase:deploy
pnpm supabase:deploy <project-ref>| Function | JWT | 用途 |
|---|---|---|
create-checkout | 开 | 已登录用户创建 Creem checkout。 |
customer-portal | 开 | 已登录用户打开 Creem customer portal。 |
activate-license | 开 | 启用 license mode 后进行可选 license-key 激活。 |
creem-webhook | 关 | Creem 调用时不带 Supabase JWT;handler 校验 webhook signature 后写入计费行。 |
functions/_shared/ 会被导入它的函数打包进去。不要单独部署 _shared。Provider
细节放在 Creem Setup 页面。
验证后端
- 使用 Supabase URL/key 启动应用,确认 renderer 能挂载。
- 注册账号、输入邮件验证码,并确认创建了
profiles行。 - 在 Showcase Todos demo 中创建、完成并删除 todos。
- 在 Account 上传 avatar,确认对象写入
avatars/<user-id>/avatar.<ext>。 - 运行
supabase functions list --project-ref <ref>,确认四个函数已部署。
