Supabase Setup
Create the backend: schema, auth callback, Edge Functions, and secrets.
Supabase is the template's backend — Postgres, Auth, Storage, and Edge Functions. This page takes you from an empty project to a fully working backend that auth, todos, uploads, subscriptions, and push all read from.
The template does not ship a complete schema. supabase/migrations/ only
contains billing_entitlements.sql (the subscriptions + payments tables).
The todos, profiles, and device_tokens tables the app reads are not
in the template — the README points at the sibling soar-expo repo, but
device_tokens doesn't exist there either. This page gives you the complete
SQL for all five tables inline so you don't have to hunt for it.
1. Create the project and grab keys
Create a project at app.supabase.com. Then, from
Project Settings → API, copy the values into your Secrets.xcconfig:
SUPABASE_URL = https:/$()/your-project-ref.supabase.co
SUPABASE_PUBLISHABLE_KEY = your-publishable-anon-key(See Configuration for the https:/$()/ trick.) The
app builds the client lazily in SupabaseClientProvider; with these two values
set, every backed feature comes online.
2. Register the auth redirect
OTP and OAuth callbacks return to the app via a custom URL scheme. Before testing sign-up, magic-link, or social sign-in, add this redirect to the allow-list under Authentication → URL Configuration → Redirect URLs:
soar://auth-callbackThe scheme is already registered in SoarStarterSwift/Info.plist
(CFBundleURLTypes). Without this allow-list entry, verification links and OAuth
round-trips dead-end. See Authentication.
3. Apply the database schema
Run the SQL below in the dashboard SQL Editor (or add it as migrations). Every
statement uses if not exists / create or replace guards, so re-applying is a
no-op — safe on a project the desktop template already seeded.
Shared trigger + profiles
profiles auto-populates from a trigger on sign-up.
create or replace function public.set_updated_at()
returns trigger language plpgsql as $$
begin new.updated_at = now(); return new; end;
$$;
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;
create policy "Users can view own profile"
on public.profiles for select using (auth.uid() = id);
create policy "Users can update own profile"
on public.profiles for update using (auth.uid() = id);
create policy "Users can insert own profile"
on public.profiles for insert with check (auth.uid() = id);
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer as $$
begin
insert into public.profiles (id, full_name)
values (new.id, new.raw_user_meta_data ->> 'full_name');
return new;
end;
$$;
create or replace trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();todos
The Todos example feature (full CRUD, ordering) reads this table.
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);
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;
create policy "Users can view their todos" on public.todos for select using (auth.uid() = user_id);
create policy "Users can insert their todos" on public.todos for insert with check (auth.uid() = user_id);
create policy "Users can update their todos" on public.todos for update using (auth.uid() = user_id);
create policy "Users can delete their todos" on public.todos for delete using (auth.uid() = user_id);device_tokens
APNs tokens land here. Columns match what PushTokenRegistrar upserts
(user_id, token, platform, updated_at) with a composite key so
re-registers are idempotent.
create table if not exists public.device_tokens (
user_id uuid not null references auth.users(id) on delete cascade,
token text not null,
platform text not null default 'ios',
updated_at timestamptz not null default now(),
primary key (user_id, token)
);
create index if not exists device_tokens_user_idx on public.device_tokens (user_id);
alter table public.device_tokens enable row level security;
create policy "Users can view own device tokens" on public.device_tokens for select using (auth.uid() = user_id);
create policy "Users can insert own device tokens" on public.device_tokens for insert with check (auth.uid() = user_id);
create policy "Users can update own device tokens" on public.device_tokens for update using (auth.uid() = user_id);
create policy "Users can delete own device tokens" on public.device_tokens for delete using (auth.uid() = user_id);subscriptions + payments (shipped)
These do ship in the template at
supabase/migrations/20260630120000_billing_entitlements.sql. Apply that file
(or supabase db push). Highlights:
subscriptions— one row per user (user_idisunique), the shared entitlement read-model. Clients get SELECT-only RLS; all writes go through the service-role Edge Function.payments— append-only order ledger, also SELECT-only for clients.- Both
cascadefromauth.users, so account deletion cleans them automatically.
This table is shared with the desktop template (soar-electron) — see
Subscriptions.
Template-fix suggestion: the cleanest long-term fix is to copy the
profiles / todos / device_tokens SQL above into soar-swift's
supabase/migrations/ so supabase db push alone stands up the whole schema.
Until that lands, the SQL here is the source of truth.
4. Create the uploads bucket
The Upload sample writes to a private Storage bucket named sample-uploads,
partitioned per user. Run this once (details on the policies in
Storage):
insert into storage.buckets (id, name, public, file_size_limit)
values ('sample-uploads', 'sample-uploads', false, 52428800)
on conflict (id) do nothing;5. Deploy the Edge Functions
Three functions live in supabase/functions/. Install the
CLI, supabase login, set
project_id in supabase/config.toml, then deploy:
# RevenueCat calls this unauthenticated — it checks the Authorization header itself
supabase functions deploy revenuecat-webhook --no-verify-jwt --project-ref <ref>
supabase functions deploy send-test-push --project-ref <ref>
supabase functions deploy delete-account --project-ref <ref>
supabase functions list --project-ref <ref> # verify all threefunctions/_shared/ (apns.ts, revenuecat.ts) is bundled into each
function automatically — never deploy it on its own.
revenuecat-webhook— writes thesubscriptionsrow from App Store events.verify_jwt = false(pinned inconfig.toml); it authenticates via theAuthorizationheader instead. See RevenueCat Setup.send-test-push— sends a test APNs push to the caller's own iOS device tokens (JWT-verified). See Push Notifications.delete-account— erases the caller's rows + uploads, then the auth user. See Authentication.
6. Set the Edge secrets
These are server-only — they never ship in the app bundle. Set them with
supabase secrets set ... or from supabase/functions/.env:
| Secret | Used by | Notes |
|---|---|---|
REVENUECAT_WEBHOOK_AUTH | revenuecat-webhook | Must match the Authorization header set on the RevenueCat webhook |
APNS_TEAM_ID | send-test-push | Apple Developer Team ID |
APNS_KEY_ID | send-test-push | Key ID for the APNs Auth Key |
APNS_BUNDLE_ID | send-test-push | e.g. com.soarstarter.SoarStarterSwift |
APNS_PRIVATE_KEY | send-test-push | Contents of the .p8 APNs Auth Key |
Do not set SUPABASE_URL, SUPABASE_ANON_KEY, or
SUPABASE_SERVICE_ROLE_KEY as secrets — the Edge runtime injects them
automatically.
Verify the backend
- Sign-up round-trips — register in the app, verify the OTP, and confirm
the user appears under Authentication → Users with a
profilesrow. - Todos persist — add a todo, force-quit, relaunch; it's still there.
- Functions deployed —
supabase functions listshows all three.
