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

Supabase para Mobile: Flutter/React Native + Supabase

Kevin Dávila

supabase mobileflutter supabasereact native supabasemobile backendsupabase tutorial

Flutter o React Native, da igual: tarde o temprano todos terminan haciendo la misma pregunta. ¿Y el backend? Ahí empieza lo divertido: auth, storage, realtime, y otra semana perdida antes de escribir la primera pantalla.

Supabase responde esa pregunta con un solo SDK que funciona igual en web y mobile. En este post vas a integrarlo en Flutter y React Native: auth nativo con biometría, storage para las fotos, realtime para el chat y modo offline.

Por qué Supabase para mobile

Supabase funciona perfecto para apps móviles porque:

  • Mismo SDK para web y mobile (supabase-js)

  • Auth nativo con biometría y deep links

  • Storage para fotos y archivos

  • Realtime para chat y notificaciones

  • Offline-first con el cliente adecuado

Flutter + Supabase

Setup

// Crear proyecto Flutter
flutter create my_app
cd my_app

// Agregar dependencias
flutter pub add supabase_flutter

Configuración

// 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://tu-proyecto.supabase.co',
    anonKey: 'tu-anon-key',
  );
  
  runApp(MyApp());
}

// Acceso global al cliente
final supabase = Supabase.instance.client;

Auth en 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();
  }

  // Usuario actual
  User? get currentUser => _supabase.auth.currentUser;
  
  // Escuchar cambios de auth
  Stream<AuthState> get authStateChanges => 
    _supabase.auth.onAuthStateChange;
}

Deep Links para OAuth

Para que OAuth funcione en mobile, configura 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 en 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 en 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

// Crear proyecto React Native
npx react-native init MyApp
cd MyApp

// Agregar dependencias
npm install @supabase/supabase-js

Configuración

// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage'

const supabaseUrl = 'https://tu-proyecto.supabase.co'
const supabaseKey = 'tu-anon-key'

export const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
})

Auth en 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 en React Native

// src/services/storage.ts
import { supabase } from '../lib/supabase'
import * as FileSystem from 'expo-file-system'

export async function uploadImage(uri: string, userId: string) {
  // Leer el archivo
  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

  // Obtener URL pública
  const { data: urlData } = supabase.storage
    .from('avatars')
    .getPublicUrl(filePath)

  return urlData.publicUrl
}

Realtime en 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

Para apps que necesitan funcionar sin conexión:

Con 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();
  }
}

Con 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

Con 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') ?? ''
  )

  // Obtener token de push del usuario
  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 })
  }

  // Enviar notificación (usando Firebase Cloud Messaging, por ejemplo)
  // ...

  return new Response(JSON.stringify({ sent: true }))
})

Errores comunes

"Invalid redirect URL"

  • Configura el deep link en el dashboard de Supabase

  • Agrega la URL en Authentication > URL Configuration

"Session not persisting"

  • En React Native, asegúrate de usar AsyncStorage

  • En Flutter, Supabase_flutter maneja esto automáticamente

"OAuth not working on mobile"

  • Configura deep links correctamente

  • Usa redirectTo con tu esquema custom

Qué sigue

Esta serie cubrió todo lo que necesitas para dominar Supabase. Desde los fundamentos hasta producción y mobile. Ahora a construir.

Si te fue útil, comparte la serie y visita codeabien.com para más contenido sobre desarrollo web y mobile.