Use SupaHost with React Native and Expo

Connect an Expo app to a SupaHost project. Install supabase-js with a storage adapter, refresh tokens when the app returns to the foreground, list rows and sign in with an emailed one-time code.

supabase-js runs in React Native, so an Expo app can use your SupaHost project directly with the public anon key. Two things differ from the web: the session needs a storage adapter, and token refresh should pause while the app is in the background.

The examples use the recipes table from the quickstart.

Install

npx create-expo-app@latest recipe-box
cd recipe-box
npx expo install @supabase/supabase-js expo-sqlite react-native-url-polyfill

expo-sqlite provides a localStorage implementation that persists across app restarts. react-native-url-polyfill fills in URL APIs supabase-js expects.

Add your project keys

Expo inlines variables that start with EXPO_PUBLIC_ into the app at build time. Create .env with the values from the Connect tab (subtab App):

EXPO_PUBLIC_SUPABASE_URL=https://app-one.acme.supahost.space
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...

Anyone can unpack a mobile app and read these values, which is fine for the anon key. Never ship the service_role key in an app.

Create the client

// lib/supabase.ts
import 'react-native-url-polyfill/auto'
import 'expo-sqlite/localStorage/install'
import { AppState, Platform } from 'react-native'
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      storage: localStorage,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  }
)

// Refresh tokens only while the app is in the foreground.
if (Platform.OS !== 'web') {
  AppState.addEventListener('change', (state) => {
    if (state === 'active') supabase.auth.startAutoRefresh()
    else supabase.auth.stopAutoRefresh()
  })
}

detectSessionInUrl is off because there's no browser address bar to read a session from.

List rows

// app/(tabs)/index.tsx
import { useEffect, useState } from 'react'
import { FlatList, Text, View } from 'react-native'
import { supabase } from '@/lib/supabase'

type Recipe = { id: number; title: string; minutes: number | null }

export default function Recipes() {
  const [recipes, setRecipes] = useState<Recipe[]>([])
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    supabase
      .from('recipes')
      .select('id, title, minutes')
      .order('created_at', { ascending: false })
      .then(({ data, error }) => {
        if (error) setError(error.message)
        else setRecipes(data)
      })
  }, [])

  if (error) return <Text>{error}</Text>
  return (
    <FlatList
      data={recipes}
      keyExtractor={(r) => String(r.id)}
      renderItem={({ item }) => (
        <View style={{ padding: 12 }}>
          <Text>{item.title} ({item.minutes} min)</Text>
        </View>
      )}
    />
  )
}

Sign in with an emailed code

Links in emails need somewhere to land. You can add your app's scheme (for example recipebox://**) to Redirect URLs on the Auth tab and handle the deep link, but the simplest mobile sign-in avoids redirects: SupaHost's magic link email also contains a one-time code, which the user types into the app.

// components/EmailCodeSignIn.tsx
import { useState } from 'react'
import { Button, Text, TextInput, View } from 'react-native'
import { supabase } from '@/lib/supabase'

export function EmailCodeSignIn() {
  const [email, setEmail] = useState('')
  const [code, setCode] = useState('')
  const [sent, setSent] = useState(false)
  const [message, setMessage] = useState('')

  async function sendCode() {
    const { error } = await supabase.auth.signInWithOtp({ email })
    if (error) setMessage(error.message)
    else setSent(true)
  }

  async function verify() {
    const { error } = await supabase.auth.verifyOtp({ email, token: code, type: 'email' })
    if (error) setMessage(error.message)
  }

  return (
    <View style={{ gap: 8, padding: 16 }}>
      <TextInput placeholder="Email" autoCapitalize="none" value={email} onChangeText={setEmail} />
      {sent && <TextInput placeholder="Code" keyboardType="number-pad" value={code} onChangeText={setCode} />}
      <Button title={sent ? 'Verify code' : 'Email me a code'} onPress={sent ? verify : sendCode} />
      {message ? <Text>{message}</Text> : null}
    </View>
  )
}

Email and password sign-in works too, with supabase.auth.signInWithPassword({ email, password }). Listen for changes anywhere in the app with supabase.auth.onAuthStateChange.

Social login on mobile

For Google and Apple, use their native sign-in SDKs to get an ID token and pass it to supabase.auth.signInWithIdToken({ provider: 'google', token }) (or 'apple'). No redirect is involved. Set the provider up on the Auth tab first; see Sign in with Google and Sign in with Apple.

Browser-based OAuth also works if you return through an https:// address your app is registered to open (iOS Universal Links or Android App Links). Add that address to Redirect URLs on the Auth tab.

Next steps

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us