Home » Supabase React Native: Build Scalable Mobile Apps with supabase react native
Latest Article

Supabase React Native: Build Scalable Mobile Apps with supabase react native

When you pair Supabase with React Native, you get a powerhouse combination for building mobile apps fast. You can stand up a full backend with authentication, a live database, and file storage without getting bogged down in server management. It's my go-to stack for turning an idea into a real, production-ready app without the usual backend headaches.

Setting Up Your Supabase React Native Project

A modern workspace featuring a laptop displaying 'SupaBase Setup', two smartphones, and a small plant.

Alright, let's get our hands dirty. Before we can write a single line of app code, we need a backend to talk to. The first stop is the Supabase dashboard to create a new project. This will become the command center for your database, user auth, and file storage.

The process is refreshingly straightforward. You'll be asked to name your project, create a strong database password (save this somewhere secure!), and pick a cloud region that’s close to your users for the best performance.

Creating Your First Supabase Project

Once you're in the dashboard, creating a new project is just a matter of filling out a simple form.

A modern workspace featuring a laptop displaying 'SupaBase Setup', two smartphones, and a small plant.

After you hit "Create new project," Supabase gets to work provisioning all the infrastructure. It usually takes a couple of minutes, and then you're redirected to your brand new project dashboard.

Right out of the box, this gives you:

  • A full-fledged Postgres database.
  • APIs that are automatically generated based on your database schema.
  • A complete user authentication system.
  • File storage buckets.
  • Serverless Edge Functions for custom backend logic.

Here’s how Supabase’s core services directly translate into the powerful features you'll build in your mobile app.

Mapping Supabase Services to React Native Features

Supabase ServiceReact Native Use CaseKey Benefit
Postgres DatabaseStoring user profiles, app data, posts, etc.A powerful, relational SQL database without the management overhead.
AuthenticationLogin, signup, password reset, social logins.Securely manage users with minimal code; integrates seamlessly.
RealtimeLive chat, notifications, collaborative features.Push data changes to clients instantly for dynamic UX.
StorageUploading profile pictures, user-generated content.Simple and scalable file handling, integrated with security policies.
Edge FunctionsSending emails, processing payments, custom logic.Run server-side code without managing a server.

This tight integration means you can focus more on the user experience in your React Native app and less on the backend plumbing.

Scaffolding Your React Native App

With the Supabase backend ready, it's time to create our React Native app. You've got two main routes: the Expo managed workflow or the Bare React Native workflow. For a detailed breakdown, check out our comprehensive React Native Expo tutorial.

We'll use Expo here because it’s the fastest way to get up and running. Just pop open your terminal and run this command:

npx create-expo-app my-supabase-app

Once that's done, jump into the new directory (cd my-supabase-app). Now we need to install the official Supabase client library, which is the bridge between our app and the backend.

npx expo install @supabase/supabase-js

A quick pro-tip: always use npx expo install instead of npm or yarn in an Expo project. It intelligently picks a library version that's compatible with your Expo SDK, which helps you avoid a lot of painful dependency mismatches down the road.

Securely Managing API Keys

Your Supabase project has two keys we need for the app: the Project URL and the anon (public) key. You can grab these from your project's dashboard under Project Settings > API.

Critical Security Note: Never, ever hardcode API keys or other secrets directly in your source code. If you commit them to a Git repository, they are exposed to the world, creating a massive security hole.

The best practice here is to use environment variables. The react-native-dotenv library makes this incredibly easy.

First, let's get it installed:

npm install react-native-dotenv

Next, we need to tell Babel to use it. Open up your babel.config.js file and add module:react-native-dotenv to the plugins array. The file should look like this:

module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['module:react-native-dotenv']
};
};

Now, create a new file named .env in the root of your project. Immediately add .env to your .gitignore file. This is a crucial step to ensure your secrets are never committed to version control.

Inside your new .env file, add your Supabase credentials:

SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Just swap in the actual values from your dashboard. With this in place, you have a secure and solid foundation for building your Supabase React Native app.

Implementing User Authentication

A hand holds a smartphone displaying a 'Send Magic Link' button for passwordless login, with a laptop in the background.

Authentication is one of those features that can make or break the user experience. A clunky login process will send people running before they even get to see the app you've worked so hard on. Fortunately, Supabase makes it surprisingly easy to add modern authentication flows to a React Native app.

Let's move beyond basic email and password forms and look at what users actually prefer.

A fantastic way to reduce friction is with passwordless login, and Magic Links are a perfect example. Instead of forcing users to invent and recall another password, you just send a secure, one-time-use link to their email.

Getting this up and running is just a single call to Supabase. When a user types in their email, you trigger this:

const { error } = await supabase.auth.signInWithOtp({
email: '[email protected]',
options: {
emailRedirectTo: 'myapp://login', // Your app's deep link
},
});

Pay close attention to that emailRedirectTo option. This is how you get the user back into your app after they've clicked the link in their email. Without proper deep linking, this whole flow falls apart on mobile.

Integrating Popular Social Logins

Another proven way to simplify signups is by offering social logins, or OAuth. Letting users sign in with an account they already have—like Google, GitHub, or Apple—is a huge win. Some studies show that adding social login can boost conversion rates by as much as 20-40% simply because it eliminates form-filling.

With Supabase, adding an OAuth provider only takes two steps:

  • Enable it in the dashboard: Head over to Authentication > Providers in your Supabase project, pick a provider like Google, and flip the switch. You'll need to paste in a Client ID and Secret, which you generate from the provider's developer console (like Google Cloud Console).
  • Call the method in your code: In your React Native app, you just need to call the signInWithOAuth method.

const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'myapp://login', // Ensure this matches your dashboard config
},
});

This will pop open the provider's login screen. Once the user authenticates, they’re sent right back to your app using the deep link you configured—the same mechanism Magic Links use.

A common pitfall with OAuth is a mismatch in redirect URLs. The redirectTo value in your code must exactly match the redirect URL you configured in the Supabase dashboard and in the provider's developer console (e.g., Google Cloud). Any small difference will cause the flow to fail.

Managing Sessions and Protected Routes

Once a user is in, your next job is to manage their session and protect certain parts of the app. Nobody wants to log in every single time they open an app. The Supabase client library handles this for you, typically using AsyncStorage under the hood to persist the session in React Native.

To create a clean separation between public and private screens, you need a routing setup that listens to the user's auth state. This is where the Supabase client really shines, because you can subscribe to authentication changes in real-time.

Here’s a hook that gives you a live session object:

const [session, setSession] = useState(null);

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});

const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});

return () => subscription.unsubscribe();
}, []);

If the session object exists, the user is logged in. Your app's root navigator can then use this state to decide what to show. If you want to dive deeper into this pattern, you can learn more about structuring a React Native app with authentication flows.

With that hook in place, the logic in your main navigation component becomes incredibly simple:

// In your main navigation component
return (

{session && session.user ? : }

);

This conditional rendering is the heart of route protection. The AuthStack can hold your login and signup screens, while the AppStack contains the core features of your application. When a user logs in or out, onAuthStateChange fires, the session state updates, and your UI automatically swaps between stacks. It’s a clean and secure way to manage the user journey.

Working with Data Using Queries and Mutations

Alright, now that users can sign in, it's time for the main event: working with data. This is where your Supabase React Native app goes from a login screen to a living, breathing product. Supabase gives us a beautiful, chainable API for all our database operations—creating, reading, updating, and deleting (CRUD).

The real magic here is that you can skip writing a whole backend API just for basic data handling. Instead, you talk directly to your Postgres database with an intuitive JavaScript client. It feels less like a separate system and more like a natural part of your React Native code.

Let's ground this in a real-world example. Imagine we're building a simple task manager app.

Fetching Data for Your UI

First things first, you need to show the user their data. Let's say you have a tasks table in Supabase and you want to pull down all the tasks for the person who is currently logged in.

The query itself is incredibly clean and readable. You just name the table, pick the columns you need, and add any filters.

Here’s how you might fetch those tasks inside a React component:

import { useState, useEffect } from 'react';
import { supabase } from '../lib/supabase'; // Your initialized client

const TaskList = () => {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchTasks = async () => {
try {
setLoading(true);
const { data: { user } } = await supabase.auth.getUser();

    if (user) {
      const { data, error, status } = await supabase
        .from('tasks')
        .select(`id, title, is_complete`)
        .eq('user_id', user.id) // Only fetch tasks for the current user
        .order('created_at', { ascending: false });

      if (error && status !== 406) {
        throw error;
      }

      if (data) {
        setTasks(data);
      }
    }
  } catch (error) {
    console.error('Error fetching tasks:', error.message);
  } finally {
    setLoading(false);
  }
};

fetchTasks();

}, []);

// … render your list of tasks using the tasks state
};

Take a look at the .eq('user_id', user.id) line. That's your security gate. It ensures that users can only see their own data, and it works hand-in-glove with the Row Level Security (RLS) policies you should have enabled on that tasks table.

Pro Tip: Always be specific with your .select() statement. Grabbing only the columns you actually need (id, title, is_complete) instead of everything with a wildcard (*) drastically cuts down on data transfer. It's a tiny change that makes a huge difference on slower mobile networks.

Creating and Modifying Data

Displaying data is great, but apps need to be interactive. Users will want to add new tasks, mark them complete, or get rid of them entirely. Supabase makes these "mutation" operations just as easy.

Let's say a user types a new task into a form. The function to handle that submission would be incredibly simple.

const handleAddTask = async (taskTitle) => {
const { data: { user } } = await supabase.auth.getUser();

if (user) {
const { error } = await supabase
.from('tasks')
.insert({ title: taskTitle, user_id: user.id });

if (error) {
  // Handle the error (e.g., show an alert)
} else {
  // Optionally, refresh the task list to show the new item
}

}
};

The .insert() method takes an object that perfectly matches the columns in your database. No guesswork.

Updating an existing task follows the same logic. To toggle a task's completion status, you'd use the .update() method and target the specific task with a filter.

const handleToggleComplete = async (taskId, currentStatus) => {
const { error } = await supabase
.from('tasks')
.update({ is_complete: !currentStatus })
.eq('id', taskId);

if (error) {
// Handle error
}
};

And of course, deleting a record is a one-liner with the .delete() method.

By chaining these simple methods together, you can build out a complete CRUD interface for your Supabase React Native app right from your frontend code. This direct-to-database approach is a massive development accelerator, freeing you up to focus on what really matters: building a fantastic user experience.

Building Live Features with Real-Time Subscriptions

Let's be honest, static apps just don't cut it anymore. Your users expect data to show up instantly, without needing to pull-to-refresh. This is where Supabase's real-time engine really shines for a Supabase React Native app, letting you build features that feel genuinely alive and collaborative.

Imagine a live notification feed, a shared to-do list that updates for everyone at once, or a chat app where messages appear the moment they're sent. Supabase handles the heavy lifting by broadcasting database changes directly to your app. You don't have to wrestle with WebSockets or manage a complex server setup—you just listen.

How Supabase Realtime Works

Under the hood, Supabase is listening for any INSERT, UPDATE, or DELETE commands in your PostgreSQL database. When a change occurs, it packages that change into a JSON message and sends it over a WebSocket connection to every connected client subscribed to that data.

This whole process is orchestrated by a dedicated Realtime server that sits between your app and the database.

Here’s a quick architectural look at how data gets from your database to your React Native app.

As the diagram shows, your app establishes a secure WebSocket connection. When you change data in your database, Postgres notifies the Realtime server, which then pushes the new payload to every authorized and subscribed client. It's an incredibly efficient system.

Setting Up a Realtime Subscription

Before you can write any code, you have to flip a switch in your Supabase dashboard. This is a crucial step that’s easy to miss. Navigate to Database > Replication and find the tables you want to make "live." You'll need to enable replication for each one.

Once that's done, you can create a subscription right inside your React Native component. The real trick is managing the subscription’s lifecycle properly to avoid memory leaks or keeping connections open when they're not needed. A useEffect hook is your best friend here.

Let's say we're building a chat room and want to see new messages pop up in a messages table.

import { useState, useEffect } from 'react';
import { supabase } from '../lib/supabase'; // Your initialized client

const ChatRoom = ({ roomId }) => {
const [messages, setMessages] = useState([]);

useEffect(() => {
// First, grab the initial chat history
const fetchMessages = async () => {
const { data } = await supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at');
setMessages(data || []);
};
fetchMessages();

// Now, set up the real-time subscription for new messages
const channel = supabase
  .channel(`room:${roomId}`)
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: `room_id=eq.${roomId}`,
    },
    (payload) => {
      // When a new message comes in, add it to our state
      setMessages((currentMessages) => [...currentMessages, payload.new]);
    }
  )
  .subscribe();

// The cleanup function is critical! Unsubscribe when the component is unmounted.
return () => {
  supabase.removeChannel(channel);
};

}, [roomId]);

// … render your chat messages using the messages state
};

Notice how we subscribe when the component mounts and, most importantly, clean up by calling supabase.removeChannel(channel) in the useEffect's return function. Skipping this cleanup is a recipe for a buggy, unstable app.

Key Takeaway: Always manage your subscriptions within the component lifecycle. This ensures you're only listening for data when the UI that needs it is actually on screen, which is fundamental for building performant real-time features.

This pattern—fetching the initial data and then subscribing to live updates—is a powerful way to build dynamic UIs. Your app loads instantly with the current state and then stays perfectly in sync as new data arrives. It’s what makes a modern Supabase React Native app feel so fluid and responsive.

Managing File Uploads and Offline Capabilities

Your app is going to live on a user's phone, which means dealing with spotty Wi-Fi and intermittent cell service. A stable connection is a luxury, not a guarantee. This brings two classic mobile development challenges to the forefront: handling file uploads and making sure your app doesn’t just die when the internet does.

Let's start with a really common feature: letting users upload a profile picture. Using Supabase Storage makes this surprisingly straightforward. The goal isn't just to get a file from A to B; it's a multi-step dance. You have to let the user pick the image, upload it to a Supabase bucket, get its public URL, and then save that URL back to their user profile.

Handling Profile Picture Uploads

To get the image from the user's device, you'll need a library like expo-image-picker. Once you have the file URI, the actual upload to Supabase is a direct shot.

Here’s a function that pulls this all together in a real-world scenario:

import * as ImagePicker from 'expo-image-picker';
import { supabase } from '../lib/supabase';

const uploadProfilePicture = async (userId) => {
// Let the user pick an image
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
});

if (result.canceled || !result.assets || result.assets.length === 0) {
return; // User backed out of the picker
}

const image = result.assets[0];
const fileExt = image.uri.split('.').pop();
const fileName = ${userId}.${fileExt};
const filePath = ${fileName};

// Supabase needs the file in a specific format for React Native
const formData = new FormData();
formData.append('file', {
uri: image.uri,
name: fileName,
type: image.type ? ${image.type}/${fileExt} : image/${fileExt},
});

// Upload the file to the 'avatars' bucket
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, formData, {
upsert: true, // This is key! It overwrites the file if it already exists.
});

if (uploadError) {
throw uploadError;
}

// Finally, update the user's profile with the new avatar URL
const { error: updateError } = await supabase
.from('profiles')
.update({ avatar_url: filePath })
.eq('id', userId);

if (updateError) {
throw updateError;
}
};

Notice the upsert: true option? That's your best friend here. It means if a user uploads a new profile picture, it automatically replaces the old one without you having to write any extra delete logic. Just remember to set up your Row Level Security (RLS) policies on the avatars bucket to control who can upload and what they can access.

Designing for Unreliable Connections

Now, what happens when that upload function runs without an internet connection? An app that just shows a spinner forever, or worse, crashes, feels broken. Your goal should be to create an experience where the user can keep working, and the app intelligently syncs up when it's back online.

This is where a local caching strategy becomes non-negotiable.

For simple needs, a fast key-value store like MMKV is brilliant for stashing temporary data. But for a true offline-first experience, you'll want a full-fledged local database. WatermelonDB is a powerhouse for this, built specifically for building reactive, offline-first React Native apps.

A proven pattern here is the "outbox queue." When a user performs an action offline (like creating a post), you don't try to send it to Supabase. Instead, you save it to your local cache with a "pending" status. A background task can then periodically check for a network connection. Once it's back, it works through the queue, sending each pending item to your backend.

This queue-and-sync approach makes the app feel responsive and resilient, and it prevents data loss. The user never even has to know their connection dropped.

This diagram gives you a sense of the lifecycle for a real-time subscription, which is another area where connection management is key.

A process flow diagram illustrates real-time subscriptions with steps for component mount, subscribe, and unsubscribe.

It’s not just about starting the subscription; it's about cleaning it up properly on unmount to avoid memory leaks and unnecessary processing.

For really complex offline requirements, dedicated sync engines can be a lifesaver. A tool like PowerSync is designed to work with backends like Supabase, keeping a local SQLite database in perfect harmony with your server. It can even handle tricky conflict resolution automatically. If you want to dive deeper into these strategies, our guide on building a React Native app with offline functionality is a great next step.

By thinking through file management and building for offline scenarios from the start, you’ll deliver a polished and dependable Supabase React Native app that users will trust.

Common Questions About Supabase and React Native

Once you get past the initial setup with Supabase and React Native, a few common "gotchas" tend to trip up even seasoned developers. I've run into these myself and have helped others navigate them plenty of times. Here are some quick, field-tested answers to get you past these hurdles without losing momentum.

How Should I Structure Row Level Security?

Let's be clear: Row Level Security (RLS) isn't just a 'nice-to-have' feature for your mobile app—it's your most critical line of defense. The most reliable way to set this up is by creating policies that tie every piece of data directly to the authenticated user's ID.

For example, on a profiles table, you’d want a policy ensuring users can only see or change their own data. The magic snippet for this is auth.uid() = user_id. This little check is your best friend.

This logic runs directly on the database server. That means even if a user manages to tinker with your app's code to request data they shouldn't have access to, the database itself will shut them down. Before you even think about shipping your app, make sure RLS is enabled on every table with user-specific or sensitive data.

I see this mistake all the time: developers rely only on client-side logic to filter data. You have to assume your client can and will be compromised. RLS is your non-negotiable gatekeeper on the server.

What Is the Best Way to Handle Environment Variables?

When it comes to managing your Supabase URL and anon key, stop searching and just use react-native-dotenv. It’s the community standard for a reason, working beautifully with both Expo and bare React Native projects.

Once installed, you’ll create a .env file in your project's root directory to hold your secrets.

SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

From there, you can pull them into your app with a simple import: import { SUPABASE_URL } from '@env';. The most important step? Add .env to your .gitignore file immediately. This one simple action prevents you from ever accidentally committing your secret keys to a public repository.

Can I Use Supabase Edge Functions from React Native?

Yes, and you absolutely should be using them. Think of Edge Functions as your secure backend toolbox. They are the perfect spot for any logic you’d never want sitting exposed on a user's phone.

Common use cases I turn to them for include:

  • Payment processing: Integrating with services like Stripe where you need to protect your secret API keys.
  • Sending notifications: Triggering transactional emails or push notifications using admin-level credentials.
  • Complex queries: Running heavy data calculations or multi-step operations that would be too slow or insecure on the client.

Calling a function from your app is straightforward. A simple invoke call does the trick: await supabase.functions.invoke('function-name', { body: { ... } });. This keeps your app lean and your secrets safely on the server.

How Do I Fix Deep Linking for OAuth Redirects?

Ah, the OAuth deep linking headache. This is a rite of passage for many developers, but the fix is usually quite simple. The problem nearly always boils down to a tiny mismatch between the redirect URL in your Supabase dashboard and the one you're passing in the app.

They have to be a 100% perfect match.

For Expo Go, the URL is often dynamic and based on your local network, something like exp://192.168.1.10:19000.

For bare React Native apps, you need to define a custom URL scheme (e.g., myapp://callback) in the native project files—that means editing Info.plist on iOS and AndroidManifest.xml on Android.

Here’s a pro tip for debugging: check your emulator or device logs right after the redirect fails. The logs will often show the exact URL the browser tried to open, immediately revealing the mismatch between what the OAuth provider sent and what your app was expecting. It makes finding the fix a whole lot easier.


At React Native Coders, we focus on in-depth guides and real-world analysis to help you build, secure, and ship high-quality mobile apps faster. Explore our tutorials to master the React Native ecosystem.

About the author

admin

Add Comment

Click here to post a comment