Docs

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.

Updated Oct 24, 2026

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 Go

A 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

#Coming from Firebase or Supabase?

PlatformWhat this is called there
FirebaseReact Native Firebase
SupabaseSupabase Expo React Native quickstart
SovariaQuickstart: React Native and Expo

Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.