Supabase Setup
Configure Supabase auth, database tables, storage, and Edge Functions for the Android template.
This page gets the Android template to a working Supabase backend: auth callbacks, PostgREST tables, the entitlement read model, FCM test push, and the RevenueCat webhook.
The Kotlin template currently ships only the device_tokens,
subscriptions, and payments migrations. todos and profiles are used
by the app but are not present in soar-kotlin/supabase/migrations/, so this
page includes the missing SQL explicitly.
Create the project
Create a Supabase project
Create a project in the Supabase dashboard and copy the project URL plus the publishable key.
Add the mobile keys
Put the client-safe values in the Android template's local.properties:
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_PUBLISHABLE_KEY=your_publishable_keyThese values become BuildConfig.SUPABASE_URL and
BuildConfig.SUPABASE_PUBLISHABLE_KEY. They are not server secrets.
Allow auth redirects
In Supabase Auth settings, add this redirect URL:
soar://auth-callbackSupabaseClientProvider installs Auth with scheme = "soar" and
host = "auth-callback", so email verification, OTP, recovery, and OAuth
callbacks need that URL to be allow-listed.
Apply the schema
From soar-kotlin/supabase, link the CLI to your project:
supabase login
supabase link --project-ref <ref>Then apply the schema in this order.
Missing profiles table
Run this SQL in the dashboard SQL editor, or copy it into a new migration in
soar-kotlin/supabase/migrations/ before 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();The app reads and updates full_name, bio, phone, location, and
avatar_url. SupabaseAuthRemoteDataSource.deleteAccount() also nulls exactly
those fields during the current client-side delete flow.
Missing todos table
Run or migrate this 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 selects by user_id, orders by sort_order
ascending then created_at descending, and writes title, completed, and
sort_order.
Template fix recommended: add first-class profiles and todos migrations
to soar-kotlin/supabase/migrations/ so supabase db push creates every
table the Android app uses.
Shipped migrations
The template does ship:
20260508000001_create_device_tokens.sql: FCM token rows, unique on(user_id, token), with select/insert/update/delete RLS for the owner.20260702000000_billing_entitlements.sql: sharedsubscriptionsandpaymentstables plus billing enums. Clients can select their own rows; writes are reserved for service-role functions.
Apply them with:
supabase db push --project-ref <ref>Most DDL uses if not exists or policy replacement, so reapplying to a shared
project is intended to be boring.
Storage bucket
The upload sample uses a private bucket named sample-uploads. Create it and
the folder-scoped policies from Storage before testing
uploads. The Kotlin app does not use the Expo sample's sample_uploads
metadata table.
Edge Functions
supabase/config.toml pins the two function auth modes:
[functions.revenuecat-webhook]
verify_jwt = false
[functions.send-test-push]
verify_jwt = trueDeploy them from the template's supabase/ folder:
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 is called by RevenueCat without a Supabase user JWT, so it
uses a static Authorization header. send-test-push is called by the signed-in
Android user, keeps JWT verification on, and reads device_tokens through that
user's RLS context. _shared/ is bundled into those functions; do not deploy it
by itself.
Edge secrets
Prop
Type
Set secrets with:
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>Do not set SUPABASE_URL, SUPABASE_ANON_KEY, or the service-role key in your
function .env; Supabase injects those at runtime.
Local function dev
Serve functions locally:
supabase functions serve --env-file supabase/functions/.envRun the helper tests:
deno test --allow-env supabase/functions/_shared/revenuecat.test.ts
deno test supabase/functions/_shared/fcm.test.tsSmoke-test the 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"
}
}'For deployed verification, replace the host with:
https://<ref>.supabase.co/functions/v1/revenuecat-webhookVerify
-
soar://auth-callbackis in the Supabase redirect allow-list. - A new sign-up creates an auth user and a matching
profilesrow. - Todos CRUD persists and survives app relaunch.
-
public.device_tokens,public.subscriptions, andpublic.paymentsexist with RLS enabled. -
supabase functions list --project-ref <ref>showsrevenuecat-webhookandsend-test-push. - The RevenueCat webhook returns
{"ok":true}for a valid smoke event.
