Codea Bien Logo
Supabase for Mobile: Flutter/React Native + Supabase
Supabase

Supabase for Mobile: Flutter/React Native + Supabase

Kevin Dávila

Kevin Dávila

supabase mobileflutter supabasereact native supabasemobile backendsupabase tutorial

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_flutter

Configuration

// 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-js

Configuration

// 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.