Expo & React Native
Set up Valet in an Expo or React Native project.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withValet } = require('valet-dev/metro')
module.exports = withValet(getDefaultConfig(__dirname))That single change configures Metro to resolve Valet's package exports and load .wasm assets. The rest of your Valet code -- schema, queries, mutations, and React hooks -- works identically to a web project.
Install
bun add valet-devOr with npm:
npm install valet-devMetro configuration
The withValet() wrapper does two things:
- Enables
unstable_enablePackageExportsso Metro resolves sub-package imports like'valet-dev/server','valet-dev/react', and'valet-dev/expo'. - Adds
wasmto the asset extensions list so the SQLite WASM backend loads on Expo web.
If you already have a custom Metro config, pass it through withValet last:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withValet } = require('valet-dev/metro')
const config = getDefaultConfig(__dirname)
// Your existing customizations
config.resolver.sourceExts.push('cjs')
module.exports = withValet(config)Define your schema
Schema definitions use the same API as web projects:
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.number(),
userId: v.string(),
}).sync({
filter: (q, ctx) => q.eq('userId', ctx.auth.userId),
mode: 'full',
}),
})Write queries and mutations
// valet/todos.ts
import { defineQuery, defineMutation, v } from 'valet-dev/server'
export const list = defineQuery({
args: {},
execution: 'local',
handler: async (ctx) => {
return ctx.db.query('todos').collect()
},
})
export const create = defineMutation({
args: { title: v.string() },
handler: async (ctx, args) => {
return ctx.db.insert('todos', {
title: args.title,
completed: 0,
userId: ctx.auth?.userId ?? '',
})
},
})Run codegen to generate typed hooks:
npx valet-dev codegenSet up ValetProvider
The generated ValetProvider handles local database initialization, SyncEngine creation, and WebSocket connection. Pass the server URL as a prop:
// App.tsx
import { ValetProvider } from './_generated/valet/react'
export default function App() {
return (
<ValetProvider url={process.env.VALET_PROJECT_URL!}>
<TodoList />
</ValetProvider>
)
}If your sync filters reference ctx.auth, pass an authToken prop:
// App.tsx
import { ValetProvider } from './_generated/valet/react'
export default function App() {
return (
<ValetProvider
url={process.env.VALET_PROJECT_URL!}
authToken={userToken}
>
<TodoList />
</ValetProvider>
)
}Use hooks in components
The generated useQuery and useMutation hooks work the same way on native and web:
// components/TodoList.tsx
import { useQuery, useMutation } from '../_generated/valet/react'
import { api } from '../_generated/valet/api'
import { View, Text, Button, FlatList } from 'react-native'
function TodoList() {
const { data: todos, isLoading } = useQuery(api.todos.list, {})
const createTodo = useMutation(api.todos.create)
if (isLoading) return <Text>Loading...</Text>
return (
<View>
<FlatList
data={todos}
keyExtractor={(item) => item._id}
renderItem={({ item }) => <Text>{item.title}</Text>}
/>
<Button
title="Add Todo"
onPress={() => createTodo.mutate({ title: 'New task' })}
/>
</View>
)
}Platform differences
Valet uses different SQLite backends depending on the platform:
| Platform | SQLite Backend |
|---|---|
| iOS | Native SQLite via expo-sqlite |
| Android | Native SQLite via expo-sqlite |
| Expo Web | SQLite compiled to WASM |
The API is identical across all three platforms. The SyncEngine abstracts the storage layer so your queries, mutations, and sync filters behave the same everywhere.
iOS and Android
On native platforms, Valet uses the device's built-in SQLite through expo-sqlite. No additional setup is required beyond the Metro configuration above.
Expo web
Expo web support uses a WASM-compiled SQLite backend. The withValet() Metro wrapper adds .wasm to the asset extensions so the WASM file loads correctly during bundling.
Offline support
Offline support works natively on mobile. When the device loses connectivity:
- Local queries (
execution: 'local') continue to read from the on-device SQLite database. - Mutations execute locally against the SQLite database and queue for sync.
- When connectivity returns, Valet reconnects, rebases pending mutations against current server state, and flushes the mutation log.
No additional configuration is needed. The SyncEngine, mutation log, and rebase logic are initialized automatically by the generated ValetProvider.
You can monitor sync status in your UI with the usePendingMutationCount and useConnectionState hooks:
// components/SyncStatus.tsx
import {
useConnectionState,
usePendingMutationCount,
} from '../_generated/valet/react'
import { Text } from 'react-native'
function SyncStatus() {
const connectionState = useConnectionState()
const pendingCount = usePendingMutationCount()
if (connectionState === 'connected' && pendingCount === 0) {
return <Text>Synced</Text>
}
if (pendingCount > 0) {
return <Text>{pendingCount} changes pending</Text>
}
return <Text>{connectionState}</Text>
}Running codegen in watch mode
During development, run codegen in watch mode so your types stay up to date as you edit schema and handler files:
npx valet-dev codegen --watch