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
- Magic links and email OTP
- Auth emails and templates to edit the code email
- Uploading files from the camera roll
- Silent disconnects when the app is in the background if you use Realtime