Expo builds Android and iOS apps from React components. This guide connects an Expo app to SurrealDB, writes a few records, and shows them on screen.
When you finish, you will have a screen that lists records read from your database.
Before you begin
You need two things:
An Expo project on SDK 54 or newer, created with
npx create-expo-appA running SurrealDB instance on your development machine
A mobile app cannot run SurrealDB on the device itself, so it always talks to a database over the network. Everything in this guide is plain JavaScript, so it works in Expo Go as well as in a development build.
1. Install the SDK
Install the SDK with npx expo install, which picks a version that matches your Expo SDK.
npx expo install surrealdb2. Create the database client
Create a file that holds a single Surreal client and the function that opens the connection. Keeping both in one module means every screen shares the same connection.
import { Surreal } from "surrealdb";
export const db = new Surreal();
export function connectToSurreal() {
return db.connect(process.env.EXPO_PUBLIC_SURREAL_ENDPOINT!, {
namespace: "example",
database: "getting_started",
authentication: {
username: "root",
password: "root",
},
});
}.connect() takes the address of your database, along with the namespace and database to work in and the credentials to sign in with. Use ws:// or wss:// addresses: a WebSocket connection stays open, so the app authenticates once instead of on every request.
Setting the address
On a phone or emulator, localhost means the device itself, not your development machine. Set EXPO_PUBLIC_SURREAL_ENDPOINT to the address that the device can actually reach.
EXPO_PUBLIC_SURREAL_ENDPOINT=ws://10.0.2.2:8000| Where the app runs | Address to use |
|---|---|
| iOS simulator | ws://127.0.0.1:8000 |
| Android emulator | ws://10.0.2.2:8000 |
| Physical device | ws://<your-machine-lan-ip>:8000 |
Variables that start with EXPO_PUBLIC_ are readable inside your app. Restart the development server after changing the file.
The root credentials above are for a local database you are experimenting with. Everything you compile into a mobile app is readable by anyone who installs it, so a released app must never carry them. Use record access to sign real users in instead.
3. Connect when the app starts
Expo Router renders app/_layout.tsx around every screen, which makes it the right place to open the connection. Wait for it before showing the rest of the app, so no screen queries a connection that is not ready.
import { useEffect, useState } from "react";
import { ActivityIndicator, Text } from "react-native";
import { Stack } from "expo-router";
import { connectToSurreal } from "../surreal";
export default function RootLayout() {
const [status, setStatus] = useState<"connecting" | "ready" | "failed">("connecting");
useEffect(() => {
connectToSurreal()
.then(() => setStatus("ready"))
.catch(() => setStatus("failed"));
}, []);
if (status === "connecting") return <ActivityIndicator />;
if (status === "failed") return <Text>Could not reach the database.</Text>;
return <Stack />;
}If you see the failure message, the address is usually the cause. Check the table in step 2 and confirm that SurrealDB is running.
4. Insert your first records
Use .create() to add a record to a table. The .content() chain holds the fields you want to store.
import { RecordId, Table } from "surrealdb";
import { db } from "./surreal";
const products = new Table("products");
// Create a record with an ID generated by the database
const [banana] = await db.create(products).content({
name: "Banana",
price: 0.8,
});
console.log(banana);
// { id: products:0dxay1r0dc9c1cn8vzuq, name: 'Banana', price: 0.8 }
// Create a record with an ID you choose
const apple = await db.create(new RecordId(products, "apple")).content({
name: "Apple",
price: 1.5,
});
console.log(apple);
// { id: products:apple, name: 'Apple', price: 1.5 }Every record has an ID made of its table name and a unique key, written as products:apple. You do not need to create the table first — SurrealDB adds it on the first write.
5. Read and display data
.select() reads records back. Pass a Table to read all of them, or a RecordId to read one. Chain .where() and .limit() to narrow the result.
import { useEffect, useState } from "react";
import { FlatList, Text, View } from "react-native";
import { Table, lt } from "surrealdb";
import { db } from "../surreal";
interface Product {
id: string;
name: string;
price: number;
}
export default function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
db.select<Product>(new Table("products"))
.where(lt("price", 1.0))
.then(setProducts)
.catch(console.error);
}, []);
return (
<FlatList
data={products}
keyExtractor={(product) => String(product.id)}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.price}</Text>
</View>
)}
/>
);
}lt is one of the expression helpers the SDK provides for building conditions. Run the app and you should see the products you created.
Next steps
You now have an Expo app that connects to SurrealDB, writes records, and reads them back. Real apps also need to sign users in, keep working when the phone locks the screen, and react to changes as they happen.
Expo SDK guide
The full integration guide: connection provider, app lifecycle, secure token storage, and live queries.
Authentication
Sign users up and in with record access instead of database credentials.
Executing queries
Learn the query builders and how to run SurrealQL statements directly.
Live queries
Receive changes from the database as they happen, without polling.
Building without Expo? The React Native guide covers the same steps, plus the polyfills a bare project needs.