Supabase 设置

为 Android 模板配置 Supabase Auth、数据库表、存储和 Edge Functions。

这一页把 Android 模板接到一个可用的 Supabase 后端:Auth 回调、 PostgREST 数据表、权益读模型、FCM 测试推送,以及 RevenueCat Webhook。

Kotlin 模板当前只随仓库提供 device_tokenssubscriptionspayments 迁移。应用代码会使用 todosprofiles,但 soar-kotlin/supabase/migrations/ 里没有对应迁移,所以本页会直接给出 缺失 SQL。

创建项目

创建 Supabase 项目

在 Supabase 控制台创建项目,并复制项目 URL 和 publishable key。

添加移动端 key

把客户端可公开使用的值写入 Android 模板的 local.properties

SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_PUBLISHABLE_KEY=your_publishable_key

它们会生成 BuildConfig.SUPABASE_URLBuildConfig.SUPABASE_PUBLISHABLE_KEY。这不是服务端密钥。

允许 Auth 回调

在 Supabase Auth 设置里添加这个 Redirect URL:

soar://auth-callback

SupabaseClientProvider 安装 Auth 时使用 scheme = "soar"host = "auth-callback",所以邮箱验证、OTP、密码恢复和 OAuth 回调都需要 这个 URL 在 allow-list 里。

应用数据库结构

soar-kotlin/supabase 目录中连接 CLI:

supabase login
supabase link --project-ref <ref>

然后按下面顺序应用 schema。

缺失的 profiles

在控制台 SQL 编辑器运行,或先复制成 soar-kotlin/supabase/migrations/ 下的新迁移,再运行 supabase db push

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 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);

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);

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();

应用会读取和更新 full_namebiophonelocationavatar_url。 当前的 SupabaseAuthRemoteDataSource.deleteAccount() 也正是把这些字段置为 null

缺失的 todos

运行或迁移这段 SQL:

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()
);

alter table public.todos
  add column if not exists sort_order integer not null default 0;

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_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);

SupabaseTodosRemoteDataSource 会按 user_id 筛选,按 sort_order 升序、 created_at 降序排序,并写入 titlecompletedsort_order

建议作为模板修复:把 profilestodos 做成正式迁移加入 soar-kotlin/supabase/migrations/,这样 supabase db push 才能创建 Android 应用实际会用到的全部表。

已随模板提供的迁移

模板已经提供:

  • 20260508000001_create_device_tokens.sql:FCM token 表,(user_id, token) 唯一,用户只能访问自己的 token。
  • 20260702000000_billing_entitlements.sql:共享的 subscriptionspayments 表以及 billing enum。客户端只能读取自己的行;写入由 service-role Edge Function 完成。

应用这些迁移:

supabase db push --project-ref <ref>

存储桶

上传示例使用名为 sample-uploads 的私有 bucket。测试上传前,请按 存储 创建 bucket 和以用户文件夹为边界的策略。Kotlin 应用没有使用 Expo 示例里的 sample_uploads 元数据表。

Edge Functions

supabase/config.toml 固定了两个函数的认证模式:

[functions.revenuecat-webhook]
verify_jwt = false

[functions.send-test-push]
verify_jwt = true

从模板的 supabase/ 目录部署:

supabase functions deploy revenuecat-webhook --no-verify-jwt --project-ref <ref>
supabase functions deploy send-test-push --project-ref <ref>
supabase functions list --project-ref <ref>

revenuecat-webhook 由 RevenueCat 调用,没有 Supabase 用户 JWT,所以使用静态 Authorization header。send-test-push 由已登录 Android 用户调用,保留 JWT 校验,并通过该用户的 RLS 上下文读取 device_tokens_shared/ 会被打包进这 两个函数,不需要单独部署。

Edge secrets

Prop

Type

设置密钥:

supabase secrets set REVENUECAT_WEBHOOK_AUTH=<long-random-string> --project-ref <ref>
supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='<service-account-json>' --project-ref <ref>

不要在函数 .env 里设置 SUPABASE_URLSUPABASE_ANON_KEY 或 service-role key;Supabase 运行时会注入这些值。

本地函数开发

supabase functions serve --env-file supabase/functions/.env

运行 helper 测试:

deno test --allow-env supabase/functions/_shared/revenuecat.test.ts
deno test supabase/functions/_shared/fcm.test.ts

本地 smoke test RevenueCat webhook:

curl -i \
  -X POST http://localhost:54321/functions/v1/revenuecat-webhook \
  -H "Authorization: ${REVENUECAT_WEBHOOK_AUTH}" \
  -H "Content-Type: application/json" \
  --data '{
    "api_version": "1.0",
    "event": {
      "type": "INITIAL_PURCHASE",
      "store": "PLAY_STORE",
      "app_user_id": "11111111-1111-1111-1111-111111111111",
      "product_id": "pro_monthly",
      "period_type": "NORMAL",
      "purchased_at_ms": 1782940800000,
      "expiration_at_ms": 1785532800000,
      "transaction_id": "gpa.test",
      "original_transaction_id": "gpa.test"
    }
  }'

部署后验证时,把 host 换成:

https://<ref>.supabase.co/functions/v1/revenuecat-webhook

验证

  • soar://auth-callback 已加入 Supabase redirect allow-list。
  • 新用户注册后会创建 auth user 和对应的 profiles 行。
  • Todos CRUD 能持久化并在重启应用后保留。
  • public.device_tokenspublic.subscriptionspublic.payments 存在且启用了 RLS。
  • supabase functions list --project-ref <ref> 能看到 revenuecat-webhooksend-test-push
  • 合法 smoke event 会让 RevenueCat webhook 返回 {"ok":true}

相关页面

On this page