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-callback

The 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_id is unique), 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 cascade from auth.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 three

functions/_shared/ (apns.ts, revenuecat.ts) is bundled into each function automatically — never deploy it on its own.

  • revenuecat-webhook — writes the subscriptions row from App Store events. verify_jwt = false (pinned in config.toml); it authenticates via the Authorization header 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:

SecretUsed byNotes
REVENUECAT_WEBHOOK_AUTHrevenuecat-webhookMust match the Authorization header set on the RevenueCat webhook
APNS_TEAM_IDsend-test-pushApple Developer Team ID
APNS_KEY_IDsend-test-pushKey ID for the APNs Auth Key
APNS_BUNDLE_IDsend-test-pushe.g. com.soarstarter.SoarStarterSwift
APNS_PRIVATE_KEYsend-test-pushContents 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 profiles row.
  • Todos persist — add a todo, force-quit, relaunch; it's still there.
  • Functions deployedsupabase functions list shows all three.

On this page