Quickstart: React Native and Expo
Build the notes app with Expo: keep the session in the device's secure store, sign in, and list and add the signed-in user's notes.
An Expo app (it runs in Expo Go) that uses the JavaScript SDK. The session lives in the device's secure storage (Keychain on iOS, Keystore on Android), so people stay signed in across launches.
#Before you start
Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.
#1. Create the app and install
npx create-expo-app@latest notes --template blank-typescript
cd notes
npx expo install expo-secure-store
npm install @sovaria/sdk#2. Add your project URL and anon key
EXPO_PUBLIC_SOVARIA_URL=https://<project-ref>.sovarias.in
EXPO_PUBLIC_SOVARIA_ANON_KEY=<anon-key>#3. Create the client with a secure session store
Secure storage on a phone is asynchronous, so the client keeps the session in memory and you save it yourself: onTokenRefreshSensitive hands you the new tokens (refresh token included) after every sign-in and refresh, and restoreSession puts them back at start-up.
import * as SecureStore from 'expo-secure-store'
import { createClient } from '@sovaria/sdk'
const KEY = 'sovaria.session'
export const sovaria = createClient(process.env.EXPO_PUBLIC_SOVARIA_URL!, process.env.EXPO_PUBLIC_SOVARIA_ANON_KEY!, {
persistSession: false,
onTokenRefreshSensitive: (tokens) => { SecureStore.setItemAsync(KEY, JSON.stringify(tokens)) },
})
// Call once before the first screen renders.
export async function restoreSession() {
const saved = await SecureStore.getItemAsync(KEY)
if (!saved) return
const t = JSON.parse(saved)
sovaria.setAccessToken(t.access_token, { expiresIn: t.expires_in }).setRefreshToken(t.refresh_token)
}
export async function signOut() {
await sovaria.auth.signOut()
await SecureStore.deleteItemAsync(KEY)
}
// data.sql answers { columns, rows, row_count }; rows are arrays in column order.
export function toObjects<T>(result: { columns: string[]; rows: unknown[][] }): T[] {
return result.rows.map((row) => Object.fromEntries(result.columns.map((c, i) => [c, row[i]])) as T)
}#4. Sign in and work with notes
import { useEffect, useState } from 'react'
import { Button, FlatList, Text, TextInput, View } from 'react-native'
import { SovariaError } from '@sovaria/sdk'
import { restoreSession, signOut, sovaria, toObjects } from './sovaria'
type Note = { id: number; body: string }
export default function App() {
const [ready, setReady] = useState(false)
const [session, setSession] = useState(sovaria.auth.getSession())
const [email, setEmail] = useState(''), [password, setPassword] = useState(''), [body, setBody] = useState('')
const [notes, setNotes] = useState<Note[]>([])
const [error, setError] = useState('')
const fail = (err: unknown) => setError(err instanceof SovariaError ? err.message : String(err))
useEffect(() => {
restoreSession().finally(() => { setSession(sovaria.auth.getSession()); setReady(true) })
return sovaria.auth.onAuthStateChange((_e, s) => setSession(s))
}, [])
const load = () => sovaria.data.sql('select id, body from notes order by id desc')
.then((r) => setNotes(toObjects<Note>(r as never))).catch(fail)
useEffect(() => { if (session) load() }, [session])
if (!ready) return null
if (!session) return (
<View style={{ padding: 24, gap: 8 }}>
<TextInput placeholder="Email" autoCapitalize="none" value={email} onChangeText={setEmail} />
<TextInput placeholder="Password" secureTextEntry value={password} onChangeText={setPassword} />
<Button title="Sign in" onPress={() => sovaria.auth.signIn(email, password).catch(fail)} />
<Button title="Create account" onPress={() => sovaria.auth.signUp(email, password).catch(fail)} />
{!!error && <Text>{error}</Text>}
</View>
)
return (
<View style={{ padding: 24, gap: 8, flex: 1 }}>
<TextInput placeholder="Write a note" value={body} onChangeText={setBody} />
<Button title="Add" onPress={() =>
sovaria.data.sql('insert into notes (body) values ($1)', [body]).then(() => { setBody(''); load() }).catch(fail)} />
{!!error && <Text>{error}</Text>}
<FlatList data={notes} keyExtractor={(n) => String(n.id)} renderItem={({ item }) => <Text>{item.body}</Text>} />
<Button title="Sign out" onPress={() => signOut()} />
</View>
)
}#5. Run it
npx expo start # scan the QR code with Expo GoA phone app has no browser origin, so allowed origins don't apply. To prove requests come from your real app and not a copy, register it and turn on attestation (App attestation).
#Next steps
- Check who can read and change
notesbefore you ship: Tutorial: prove your table is private before you ship. - Push new notes to other devices as they happen: Database change events.
- Push notifications and app registration: Mobile apps.
- Keep working offline with the document cache: Offline-first apps.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | React Native Firebase |
| Supabase | Supabase Expo React Native quickstart |
| Sovaria | Quickstart: React Native and Expo |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.