Supabase

PreviousNext

A React Native starter with BNA UI components and a Supabase backend — Postgres, row level security, realtime, storage and edge functions, with or without authentication.

Supabase is Postgres with an API in front of it. You write SQL migrations and row level security policies; the client library gives you typed queries, realtime subscriptions, file storage, authentication and Deno edge functions against the same database.

BNA UI ships two Supabase scaffolds: one with a backend and no sign-in, one with authentication and the screens already built.

The auth starter

Built around the same principles as the rest of BNA UI:

  • Open Code: the authentication screens, the hooks and the SQL migrations are copied into your project, not imported from a package.
  • Mobile-First: flows designed for React Native, with the session encrypted in the platform keychain.
  • Cross-Platform: iOS, Android and web, from one code path.
  • Secure by default: row level security on every table, scoped by auth.uid() — not by what the client asks for.

Sign-in methods

MethodMechanismShips with a screen
Email + passwordsignInWithPasswordYes
Magic linksignInWithOtp + deep linkYes
Email OTPsignInWithOtp + verifyOtpYes
GooglesignInWithOAuth, browser PKCEYes
ApplesignInWithOAuth, browser PKCEYes
GitHubsignInWithOAuth, browser PKCEYes

All three OAuth providers share one code path, so adding a fourth is a line in an array.

What lands in your project

lib/
├── supabase.ts             the client
├── large-secure-store.ts   AES-256 session storage
├── realtime.ts             the postgres_changes reducer, as a pure function
└── database.types.ts       generated from your schema
providers/auth-provider.tsx session, profile, deep links
app/(auth)/                 six screens
app/(onboarding)/           intro carousel + profile setup
supabase/migrations/        profiles, tasks, storage — with RLS on all of them
supabase/functions/         hello-world, delete-account

The two layers of access control

The route guards in app/_layout.tsx decide what renders:

app/_layout.tsx
<Stack.Protected guard={!signedIn}>
  <Stack.Screen name='(auth)' />
</Stack.Protected>
<Stack.Protected guard={signedIn && !needsOnboarding}>
  <Stack.Screen name='(tabs)' />
</Stack.Protected>

The RLS policies decide what the database will actually return:

supabase/migrations/0002_tasks.sql
create policy "Users can read their own tasks"
  on public.tasks for select
  to authenticated
  using (auth.uid() = user_id);

Only the second one is a security boundary. The first is there so users are not looking at empty screens.

Reading the signed-in user

import { useAuth } from '@/providers/auth-provider';
 
const { user, profile, loading, signOut } = useAuth();

profile is the row from public.profiles, kept current over a realtime subscription. user is the auth.users record Supabase owns.

Environment

VariableWhere it livesSet byUsed for
EXPO_PUBLIC_SUPABASE_URL.env.localbna-ui supabaseBuilding the client
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY.env.localbna-ui supabaseBuilding the client
SUPABASE_SERVICE_ROLE_KEYEdge functionsSupabaseServer-side work that bypasses RLS
SUPABASE_ANON_KEYEdge functionsSupabaseServer-side work that respects RLS

Provider credentials — Google, Apple, GitHub, SMTP — live in the Supabase dashboard, not in any file in your repository.

Guides

Learn more