Supabase for Mobile: Flutter/React Native + Supabase

Kevin Dávila
Flutter or React Native, it doesn't matter: sooner or later everyone ends up asking the same question. And who builds the backend? That's where the fun begins: auth, storage, realtime, and another lost week before you write your first screen.
Supabase answers that question with a single SDK that works the same on web and mobile. In this post you'll integrate it into Flutter and React Native: native auth with biometrics, storage for photos, realtime chat, and offline mode.
Why Supabase for mobile
Supabase works perfectly for mobile apps because:
Same SDK for web and mobile (supabase-js)
Native auth with biometrics and deep links
Storage for photos and files
Realtime for chat and notifications
Offline-first with the right client
Flutter + Supabase
Setup
// Create Flutter project
flutter create my_app
cd my_app
// Add dependencies
flutter pub add supabase_flutterConfiguration
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'https://your-project.supabase.co',
anonKey: 'your-anon-key',
);
runApp(MyApp());
}
// Global client access
final supabase = Supabase.instance.client;Auth in Flutter
// lib/services/auth_service.dart
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthService {
final _supabase = Supabase.instance.client;
// Email/Password
Future<AuthResponse> signIn(String email, String password) async {
return await _supabase.auth.signInWithPassword(
email: email,
password: password,
);
}
Future<AuthResponse> signUp(String email, String password) async {
return await _supabase.auth.signUp(
email: email,
password: password,
);
}
// Google OAuth
Future<void> signInWithGoogle() async {
await _supabase.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo: 'io.supabase.myapp://login-callback/',
);
}
// Logout
Future<void> signOut() async {
await _supabase.auth.signOut();
}
// Current user
User? get currentUser => _supabase.auth.currentUser;
// Listen to auth changes
Stream<AuthState> get authStateChanges =>
_supabase.auth.onAuthStateChange;
}Deep Links for OAuth
For OAuth to work on mobile, configure deep links:
Android (android/app/src/main/AndroidManifest.xml):
<manifest>
<application>
<activity>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="io.supabase.myapp"
android:host="login-callback" />
</intent-filter>
</activity>
</application>
</manifest>iOS (ios/Runner/Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>io.supabase.myapp</string>
</array>
</dict>
</array>Queries in Flutter
// lib/services/task_service.dart
import 'package:supabase_flutter/supabase_flutter.dart';
class TaskService {
final _supabase = Supabase.instance.client;
Future<List<Map<String, dynamic>>> getTasks() async {
final user = _supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
final data = await _supabase
.from('tasks')
.select()
.eq('user_id', user.id)
.order('created_at', ascending: false);
return List<Map<String, dynamic>>.from(data);
}
Future<void> createTask(String title) async {
final user = _supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
await _supabase.from('tasks').insert({
'user_id': user.id,
'title': title,
});
}
Future<void> toggleTask(String taskId, bool completed) async {
await _supabase
.from('tasks')
.update({'completed': completed})
.eq('id', taskId);
}
}Realtime in Flutter
// lib/services/realtime_service.dart
import 'package:supabase_flutter/supabase_flutter.dart';
class RealtimeService {
final _supabase = Supabase.instance.client;
RealtimeChannel? _channel;
void listenToTasks(void Function(Map<String, dynamic>) onInsert) {
_channel = _supabase
.channel('tasks')
.onPostgresChanges(
event: PostgresChangeEvent.insert,
schema: 'public',
table: 'tasks',
callback: (payload) {
onInsert(payload.newRecord);
},
)
.subscribe();
}
void stopListening() {
_channel?.unsubscribe();
}
}React Native + Supabase
Setup
// Create React Native project
npx react-native init MyApp
cd MyApp
// Add dependencies
npm install @supabase/supabase-jsConfiguration
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage'
const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = 'your-anon-key'
export const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})Auth in React Native
// src/services/auth.ts
import { supabase } from '../lib/supabase'
export async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
return data
}
export async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
})
if (error) throw error
return data
}
export async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'myapp://login-callback',
},
})
if (error) throw error
return data
}
export async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}Storage in React Native
// src/services/storage.ts
import { supabase } from '../lib/supabase'
export async function uploadImage(uri: string, userId: string) {
// Read file
const response = await fetch(uri)
const blob = await response.blob()
const filePath = `${userId}/${Date.now()}.jpg`
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, blob, {
contentType: 'image/jpeg',
})
if (error) throw error
// Get public URL
const { data: urlData } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
return urlData.publicUrl
}Realtime in React Native
// src/services/realtime.ts
import { supabase } from '../lib/supabase'
export function listenToMessages(
roomId: string,
onMessage: (msg: any) => void
) {
const channel = supabase
.channel(`room-${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`,
},
(payload) => {
onMessage(payload.new)
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}Offline-first
For apps that need to work without connection:
With Flutter
// lib/services/offline_service.dart
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class OfflineService {
final _supabase = Supabase.instance.client;
final _pendingActions = <Future Function()>[];
Future<void> executeWithOfflineSupport(
Future Function() action,
) async {
final connectivity = await Connectivity().checkConnectivity();
if (connectivity == ConnectivityResult.none) {
_pendingActions.add(action);
return;
}
await action();
}
Future<void> syncPendingActions() async {
for (final action in _pendingActions) {
await action();
}
_pendingActions.clear();
}
}With React Native
// src/services/offline.ts
import NetInfo from '@react-native-community/netinfo'
import { supabase } from '../lib/supabase'
class OfflineManager {
private pendingOps: Array<() => Promise<void>> = []
private isOnline = true
constructor() {
NetInfo.addEventListener(state => {
const wasOffline = !this.isOnline
this.isOnline = state.isConnected ?? false
if (wasOffline && this.isOnline) {
this.syncPending()
}
})
}
async execute(op: () => Promise<void>) {
if (this.isOnline) {
await op()
} else {
this.pendingOps.push(op)
}
}
private async syncPending() {
for (const op of this.pendingOps) {
await op()
}
this.pendingOps = []
}
}
export const offlineManager = new OfflineManager()Push Notifications
With Supabase Edge Functions
// supabase/functions/send-notification/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const { userId, title, body } = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Get user's push token
const { data: profile } = await supabase
.from('profiles')
.select('push_token')
.eq('id', userId)
.single()
if (!profile?.push_token) {
return new Response('No push token', { status: 400 })
}
// Send notification (using Firebase Cloud Messaging, for example)
// ...
return new Response(JSON.stringify({ sent: true }))
})Common errors
"Invalid redirect URL"
Configure the deep link in Supabase dashboard
Add the URL in Authentication > URL Configuration
"Session not persisting"
In React Native, make sure to use AsyncStorage
In Flutter, Supabase_flutter handles this automatically
"OAuth not working on mobile"
Configure deep links correctly
Use redirectTo with your custom scheme
What's next
This series covered everything you need to master Supabase. From fundamentals to production and mobile. Now go build.
If you found it useful, share the series and visit codeabien.com for more content on web and mobile development.