What is PocketBase?
PocketBase is an awesome little backend as a service (BaaS) that runs on Go. Its feature set includes an embedded (SQLite) database with real-time subscriptions and built-in auth management.
Client-side SDKs are available for both Dart and JavaScript, which makes it incredibly easy to get up and running within a React Native or Flutter mobile project.
Project Setup
In this tutorial, we're going to build a simple app that includes a login screen, an account creation screen, and a few screens that require the user to be authenticated to view.
Let's start by setting up a minimal React Native Expo application using the expo-router "tabs" template.
npx create-expo-app@latest
💡
Pro tip - see a list of expo templates by running npx create-expo-app --templates
We'll also need some other packages to make all of this work. Let's install pocketbase, and @react-native-async-storage/async-storage. We'll need AsyncStorage to persist sessions within React Native. This is referenced here in the JavaScript SDK docs.
Since we're using Expo, use this command to install the packages:
npx expo install pocketbase @react-native-async-storage/async-storage
Create a PocketBase context provider and hook
Use a context provider and a custom hook to initialize our PocketBase instance within the app. Putting this in a context and provider will allow us to easily access our PocketBase instance anywhere in the app.
Create a folder at the root of the project and name it context. Within that folder, create a file named pocketbase.jsx.

In pocketbase.jsx:
import React, { createContext, useContext, useState, useEffect } from 'react';
import PocketBase, { AsyncAuthStore } from 'pocketbase';
import AsyncStorage from '@react-native-async-storage/async-storage';
const PocketBaseContext = createContext();
export const usePocketBase = () => useContext(PocketBaseContext);
export const PocketBaseProvider = ({ children }) => {
const [pb, setPb] = useState();
useEffect(() => {
const initializePocketBase = async () => {
const store = new AsyncAuthStore({
save: async (serialized) => AsyncStorage.setItem('pb_auth', serialized),
initial: await AsyncStorage.getItem('pb_auth'),
clear: async () => AsyncStorage.removeItem('pb_auth'),
});
const pbInstance = new PocketBase('<your-pocketbase-url>', store);
setPb(pbInstance);
};
initializePocketBase();
}, []);
return (
<PocketBaseContext.Provider value={{ pb }}>
{children}
</PocketBaseContext.Provider>
);
};
You'll notice that when we initialize PocketBase, we instantiate a new AsyncAuthStore. This is responsible for storing our user's session data in AsyncStorage. We then pass that store to the new Pocketbase instance.
In app/_layout.jsx, we'll wrap the RootLayout with the provider we just created.
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import React, { useEffect } from 'react';
import { PocketBaseProvider } from '@/context/pocketbase';
import { useColorScheme } from '@/components/useColorScheme';
export {
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
initialRouteName: '(tabs)',
};
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<PocketBaseProvider>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
<Stack.Screen name='modal' options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
</PocketBaseProvider>
);
}
Create an auth context provider and hook
We want to access the user's authentication status from anywhere in the app. Let's create an AuthContext and a useAuth hook that will export our user's authentication status and the methods needed to sign in, create an account, and sign out.
Create a file named auth.jsx in the context folder:
import { useSegments, useRouter, useNavigationContainerRef } from 'expo-router';
import { useState, useEffect, createContext, useContext } from 'react';
import { usePocketBase } from './pocketbase';
const AuthContext = createContext({});
export function useAuth() {
return useContext(AuthContext);
}
function useProtectedRoute(user, isInitialized) {
const router = useRouter();
const segments = useSegments();
const [isNavigationReady, setIsNavigationReady] = useState(false);
const rootNavRef = useNavigationContainerRef();
useEffect(() => {
const unsubscribe = rootNavRef?.addListener('state', (event) => {
setIsNavigationReady(true);
});
return function cleanup() {
if (unsubscribe) {
unsubscribe();
}
};
}, [rootNavRef.current]);
useEffect(() => {
if (!isNavigationReady) return;
const inAuthGroup = segments[0] === '(auth)';
if (!isInitialized) return;
if (
!user &&
!inAuthGroup
) {
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
router.replace('/(tabs)');
}
}, [user, segments, isNavigationReady, isInitialized]);
}
export function AuthProvider({ children }) {
const { pb } = usePocketBase();
const [isInitialized, setIsInitialized] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
const checkAuthStatus = async () => {
if (pb) {
const isLoggedIn = pb.authStore.isValid;
setIsLoggedIn(isLoggedIn);
setUser(isLoggedIn ? pb.authStore.model : null);
setIsInitialized(true);
}
};
checkAuthStatus();
}, [pb]);
const appSignIn = async (email, password) => {
if (!pb) return { error: 'PocketBase not initialized' };
try {
const resp = await pb
?.collection('users')
.authWithPassword(email, password);
setUser(pb?.authStore.isValid ? pb.authStore.model : null);
setIsLoggedIn(pb?.authStore.isValid ?? false);
return { user: resp?.record };
} catch (e) {
return { error: e };
}
};
const appSignOut = async () => {
if (!pb) return { error: 'PocketBase not initialized' };
try {
await pb?.authStore.clear();
setUser(null);
setIsLoggedIn(false);
return { user: null };
} catch (e) {
return { error: e };
}
};
const createAccount = async ({ email, password, passwordConfirm, name }) => {
if (!pb) return { error: 'PocketBase not initialized' };
try {
const resp = await pb.collection('users').create({
email,
password,
passwordConfirm,
name: name ?? '',
});
return { user: resp };
} catch (e) {
return { error: e.response };
}
};
useProtectedRoute(user, isInitialized);
return (
<AuthContext.Provider
value={{
signIn: (email, password) => appSignIn(email, password),
signOut: () => appSignOut(),
createAccount: ({ email, password, passwordConfirm, name }) =>
createAccount({ email, password, passwordConfirm, name }),
isLoggedIn,
isInitialized,
user,
}}
>
{children}
</AuthContext.Provider>
);
}
A lot is going on in there! I commented on some of the items that may not be as clear as others. One of the things I struggled with was the navigation state not being initialized before checking for the auth state of the user. After doing some digging in the expo router docs, I found that useNavigationContainerRef() is useful for checking to see if the <NavigationContainer /> has mounted.
The high level of the flow in this context is:
Wait for the navigation to mount
Check and see if we're in the (auth) segment. (This is the part of the app that requires no auth).
If we're not logged in and not in the (auth) segment, redirect to login .
If we're logged in and we're in the (auth) segment, redirect to home .
Export our user, logged-in state, and auth methods.
💡
useProtectedRoute performs the check for steps 2-4. This check will be performed on every render.
Wrap the root _layout.jsx with the AuthProvider
function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<PocketBaseProvider>
<AuthProvider>
<ThemeProvider
value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
>
<Stack>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
<Stack.Screen name='modal' options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
</AuthProvider>
</PocketBaseProvider>
);
}
Set up our app structure and root index.jsx
As described above, we need to separate our app into "segments". One segment will be screens that don't require auth - log in, create an account, reset your password, etc. The other segment will be "protected routes". Or, screens that require us to be logged in.
Things are ok right now, but there's a little "flash" of the home screen when we start the app.

We can fix this by including a index.jsx file at the root of our app that uses the isInitialized value exported from the auth context.

import { View, ActivityIndicator } from 'react-native';
import { useAuth } from '@/context/auth';
import { useRootNavigationState, useRouter, useSegments } from 'expo-router';
import { useEffect } from 'react';
export default function Index() {
const { isInitialized, isLoggedIn } = useAuth();
const router = useRouter();
const segments = useSegments();
const navigationState = useRootNavigationState();
useEffect(() => {
if (!isInitialized || !navigationState?.key) return;
const inAuthGroup = segments[0] === '(auth)';
if (
!isLoggedIn &&
!inAuthGroup
) {
router.replace('/(auth)/login');
} else if (isLoggedIn) {
router.replace('/(tabs)');
}
}, [segments, navigationState?.key, isInitialized]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
{!navigationState?.key ? <ActivityIndicator /> : <></>}
</View>
);
}
A lot of this should look familiar. It follows the same pattern as the AuthContextbut performs the checks on app launch. Screen-flash eliminated!

That's it!
I won't cover the login and account creation UI. You can find all of the code for this article on GitHub HERE!
If you loved this article, please let me know! Press the like button below, send me a DM on Twitter @ctrlaltideate, or buy me a coffee 🤗.
References
PocketBase docs
PocketBase JavaScript SDK docs
Expo Router Quick Start