🗄️ Database Guide

Index

  1. Database Options
  2. Local Storage (AsyncStorage)
  3. Local SQLite
  4. Supabase (PostgreSQL)
  5. Firebase Firestore
  6. Custom Backend with Prisma
  7. Sync Patterns
  8. Migrations and Schemas

Database Options

OptionTypeBest forProsCons
AsyncStorageLocal (Key-Value)Preferences, simple cacheSimple, includedNot relational, size limit
SQLiteLocal (SQL)Offline-first apps, structured dataRelational, fast, offlineLocal only, no sync
SupabaseCloud (PostgreSQL)Apps with users, real-timeSQL, built-in Auth, real-timeRequires connection
Firebase FirestoreCloud (NoSQL)Simple apps, real-timeEasy setup, scalableLimited queries, costly at scale
Backend + PrismaCloud (SQL)Full control, complex appsFlexible, typedMore development

Quick Decision

Do you need sync across devices?
├── NO → AsyncStorage or SQLite
└── YES → Do you need complex SQL queries?
    ├── YES → Supabase or custom backend
    └── NO → Firebase Firestore

Local Storage (AsyncStorage)

Instalación

npx expo install @react-native-async-storage/async-storage

Uso Básico

import AsyncStorage from '@react-native-async-storage/async-storage';

// Guardar string
await AsyncStorage.setItem('user_token', 'abc123');

// Guardar objeto (JSON)
await AsyncStorage.setItem('user_preferences', JSON.stringify({
  theme: 'dark',
  language: 'es',
  notifications: true,
}));

// Leer
const token = await AsyncStorage.getItem('user_token');
const prefsString = await AsyncStorage.getItem('user_preferences');
const prefs = prefsString ? JSON.parse(prefsString) : null;

// Eliminar
await AsyncStorage.removeItem('user_token');

// Eliminar múltiples
await AsyncStorage.multiRemove(['key1', 'key2', 'key3']);

// Limpiar todo
await AsyncStorage.clear();

// Obtener todas las keys
const keys = await AsyncStorage.getAllKeys();

Hook Personalizado para AsyncStorage

// hooks/useAsyncStorage.ts
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

export function useAsyncStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(initialValue);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const loadValue = async () => {
      try {
        const item = await AsyncStorage.getItem(key);
        if (item !== null) {
          setStoredValue(JSON.parse(item));
        }
      } catch (error) {
        console.error('Error loading from AsyncStorage:', error);
      } finally {
        setIsLoading(false);
      }
    };

    loadValue();
  }, [key]);

  const setValue = useCallback(async (value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Error saving to AsyncStorage:', error);
    }
  }, [key, storedValue]);

  const removeValue = useCallback(async () => {
    try {
      await AsyncStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error('Error removing from AsyncStorage:', error);
    }
  }, [key, initialValue]);

  return { value: storedValue, setValue, removeValue, isLoading };
}

Integración con React Query

// contexts/DataContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: string;
}

const STORAGE_KEY = 'todos';

export const [TodoProvider, useTodos] = createContextHook(() => {
  const queryClient = useQueryClient();

  // Leer datos
  const todosQuery = useQuery({
    queryKey: ['todos'],
    queryFn: async (): Promise<Todo[]> => {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      return stored ? JSON.parse(stored) : [];
    },
  });

  // Guardar datos
  const saveMutation = useMutation({
    mutationFn: async (todos: Todo[]) => {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
      return todos;
    },
    onSuccess: (data) => {
      queryClient.setQueryData(['todos'], data);
    },
  });

  // Agregar todo
  const addTodo = (title: string) => {
    const newTodo: Todo = {
      id: Date.now().toString(),
      title,
      completed: false,
      createdAt: new Date().toISOString(),
    };
    const updated = [...(todosQuery.data || []), newTodo];
    saveMutation.mutate(updated);
  };

  // Toggle completado
  const toggleTodo = (id: string) => {
    const updated = (todosQuery.data || []).map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    saveMutation.mutate(updated);
  };

  // Eliminar todo
  const deleteTodo = (id: string) => {
    const updated = (todosQuery.data || []).filter(todo => todo.id !== id);
    saveMutation.mutate(updated);
  };

  return {
    todos: todosQuery.data || [],
    isLoading: todosQuery.isLoading,
    addTodo,
    toggleTodo,
    deleteTodo,
  };
});

SQLite Local

Instalación

npx expo install expo-sqlite

Configuración y Uso

// lib/database.ts
import * as SQLite from 'expo-sqlite';

const db = SQLite.openDatabaseSync('myapp.db');

// Inicializar tablas
export async function initDatabase() {
  await db.execAsync(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      email TEXT UNIQUE NOT NULL,
      name TEXT NOT NULL,
      avatar TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE IF NOT EXISTS todos (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      title TEXT NOT NULL,
      description TEXT,
      completed INTEGER DEFAULT 0,
      due_date DATETIME,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users (id)
    );

    CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos (user_id);
  `);
}

// Operaciones CRUD
export const todosDB = {
  async getAll(userId: number) {
    return db.getAllAsync<Todo>(
      'SELECT * FROM todos WHERE user_id = ? ORDER BY created_at DESC',
      [userId]
    );
  },

  async getById(id: number) {
    return db.getFirstAsync<Todo>(
      'SELECT * FROM todos WHERE id = ?',
      [id]
    );
  },

  async create(todo: Omit<Todo, 'id' | 'created_at'>) {
    const result = await db.runAsync(
      'INSERT INTO todos (user_id, title, description, completed, due_date) VALUES (?, ?, ?, ?, ?)',
      [todo.user_id, todo.title, todo.description || null, todo.completed ? 1 : 0, todo.due_date || null]
    );
    return result.lastInsertRowId;
  },

  async update(id: number, updates: Partial<Todo>) {
    const fields = Object.keys(updates)
      .map(key => `${key} = ?`)
      .join(', ');
    const values = Object.values(updates);
    
    await db.runAsync(
      `UPDATE todos SET ${fields} WHERE id = ?`,
      [...values, id]
    );
  },

  async delete(id: number) {
    await db.runAsync('DELETE FROM todos WHERE id = ?', [id]);
  },

  async toggleCompleted(id: number) {
    await db.runAsync(
      'UPDATE todos SET completed = NOT completed WHERE id = ?',
      [id]
    );
  },
};

Hook para SQLite con React Query

// hooks/useSQLiteTodos.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { todosDB } from '@/lib/database';

export function useSQLiteTodos(userId: number) {
  const queryClient = useQueryClient();

  const todosQuery = useQuery({
    queryKey: ['todos', userId],
    queryFn: () => todosDB.getAll(userId),
  });

  const createMutation = useMutation({
    mutationFn: (todo: Omit<Todo, 'id' | 'created_at'>) => todosDB.create(todo),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  const updateMutation = useMutation({
    mutationFn: ({ id, updates }: { id: number; updates: Partial<Todo> }) =>
      todosDB.update(id, updates),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  const deleteMutation = useMutation({
    mutationFn: (id: number) => todosDB.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  return {
    todos: todosQuery.data || [],
    isLoading: todosQuery.isLoading,
    create: createMutation.mutate,
    update: updateMutation.mutate,
    delete: deleteMutation.mutate,
    isCreating: createMutation.isPending,
  };
}

Supabase (PostgreSQL)

Instalación

npx expo install @supabase/supabase-js

Configuración

// lib/supabase.ts
import 'react-native-url-polyfill/auto';
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;

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

// Tipos generados (puedes generarlos con supabase gen types)
export interface Database {
  public: {
    Tables: {
      profiles: {
        Row: {
          id: string;
          email: string;
          name: string;
          avatar_url: string | null;
          created_at: string;
        };
        Insert: Omit<Database['public']['Tables']['profiles']['Row'], 'created_at'>;
        Update: Partial<Database['public']['Tables']['profiles']['Insert']>;
      };
      todos: {
        Row: {
          id: number;
          user_id: string;
          title: string;
          description: string | null;
          completed: boolean;
          due_date: string | null;
          created_at: string;
        };
        Insert: Omit<Database['public']['Tables']['todos']['Row'], 'id' | 'created_at'>;
        Update: Partial<Database['public']['Tables']['todos']['Insert']>;
      };
    };
  };
}

Operaciones CRUD con Supabase

// lib/supabase-db.ts
import { supabase, Database } from './supabase';

type Todo = Database['public']['Tables']['todos']['Row'];
type TodoInsert = Database['public']['Tables']['todos']['Insert'];
type TodoUpdate = Database['public']['Tables']['todos']['Update'];

export const supabaseTodos = {
  async getAll(userId: string): Promise<Todo[]> {
    const { data, error } = await supabase
      .from('todos')
      .select('*')
      .eq('user_id', userId)
      .order('created_at', { ascending: false });

    if (error) throw error;
    return data || [];
  },

  async getById(id: number): Promise<Todo | null> {
    const { data, error } = await supabase
      .from('todos')
      .select('*')
      .eq('id', id)
      .single();

    if (error) throw error;
    return data;
  },

  async create(todo: TodoInsert): Promise<Todo> {
    const { data, error } = await supabase
      .from('todos')
      .insert(todo)
      .select()
      .single();

    if (error) throw error;
    return data;
  },

  async update(id: number, updates: TodoUpdate): Promise<Todo> {
    const { data, error } = await supabase
      .from('todos')
      .update(updates)
      .eq('id', id)
      .select()
      .single();

    if (error) throw error;
    return data;
  },

  async delete(id: number): Promise<void> {
    const { error } = await supabase
      .from('todos')
      .delete()
      .eq('id', id);

    if (error) throw error;
  },
};

Suscripciones en Tiempo Real

// hooks/useRealtimeTodos.ts
import { useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
import { supabaseTodos } from '@/lib/supabase-db';

export function useRealtimeTodos(userId: string) {
  const queryClient = useQueryClient();

  const todosQuery = useQuery({
    queryKey: ['todos', userId],
    queryFn: () => supabaseTodos.getAll(userId),
  });

  useEffect(() => {
    // Suscribirse a cambios en tiempo real
    const channel = supabase
      .channel('todos-changes')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'todos',
          filter: `user_id=eq.${userId}`,
        },
        (payload) => {
          console.log('Cambio recibido:', payload);
          
          // Invalidar query para refetch
          queryClient.invalidateQueries({ queryKey: ['todos', userId] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [userId, queryClient]);

  return todosQuery;
}

Contexto Completo con Supabase

// contexts/SupabaseDataContext.tsx
import createContextHook from '@nkzw/create-context-hook';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { supabaseTodos } from '@/lib/supabase-db';
import { useAuth } from './AuthContext';

export const [SupabaseDataProvider, useSupabaseData] = createContextHook(() => {
  const queryClient = useQueryClient();
  const { user } = useAuth();
  const userId = user?.id;

  // Query de todos
  const todosQuery = useQuery({
    queryKey: ['todos', userId],
    queryFn: () => supabaseTodos.getAll(userId!),
    enabled: !!userId,
  });

  // Suscripción en tiempo real
  useEffect(() => {
    if (!userId) return;

    const channel = supabase
      .channel('todos-realtime')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'todos',
          filter: `user_id=eq.${userId}`,
        },
        () => {
          queryClient.invalidateQueries({ queryKey: ['todos', userId] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [userId, queryClient]);

  // Mutations
  const createMutation = useMutation({
    mutationFn: (data: { title: string; description?: string }) =>
      supabaseTodos.create({
        user_id: userId!,
        title: data.title,
        description: data.description || null,
        completed: false,
        due_date: null,
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  const updateMutation = useMutation({
    mutationFn: ({ id, updates }: { id: number; updates: any }) =>
      supabaseTodos.update(id, updates),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  const deleteMutation = useMutation({
    mutationFn: (id: number) => supabaseTodos.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos', userId] });
    },
  });

  return {
    todos: todosQuery.data || [],
    isLoading: todosQuery.isLoading,
    error: todosQuery.error,
    
    createTodo: createMutation.mutateAsync,
    updateTodo: updateMutation.mutateAsync,
    deleteTodo: deleteMutation.mutateAsync,
    
    isCreating: createMutation.isPending,
    isUpdating: updateMutation.isPending,
    isDeleting: deleteMutation.isPending,
  };
});

Firebase Firestore

Instalación

npx expo install firebase

Configuración

// lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);

Operaciones CRUD con Firestore

// lib/firestore-db.ts
import {
  collection,
  doc,
  getDocs,
  getDoc,
  addDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  onSnapshot,
  Timestamp,
} from 'firebase/firestore';
import { db } from './firebase';

interface Todo {
  id: string;
  userId: string;
  title: string;
  description?: string;
  completed: boolean;
  dueDate?: Date;
  createdAt: Date;
}

const todosCollection = collection(db, 'todos');

export const firestoreTodos = {
  async getAll(userId: string): Promise<Todo[]> {
    const q = query(
      todosCollection,
      where('userId', '==', userId),
      orderBy('createdAt', 'desc')
    );
    
    const snapshot = await getDocs(q);
    return snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data(),
      createdAt: doc.data().createdAt?.toDate(),
      dueDate: doc.data().dueDate?.toDate(),
    })) as Todo[];
  },

  async getById(id: string): Promise<Todo | null> {
    const docRef = doc(db, 'todos', id);
    const docSnap = await getDoc(docRef);
    
    if (!docSnap.exists()) return null;
    
    return {
      id: docSnap.id,
      ...docSnap.data(),
      createdAt: docSnap.data().createdAt?.toDate(),
      dueDate: docSnap.data().dueDate?.toDate(),
    } as Todo;
  },

  async create(todo: Omit<Todo, 'id' | 'createdAt'>): Promise<string> {
    const docRef = await addDoc(todosCollection, {
      ...todo,
      createdAt: Timestamp.now(),
      dueDate: todo.dueDate ? Timestamp.fromDate(todo.dueDate) : null,
    });
    return docRef.id;
  },

  async update(id: string, updates: Partial<Todo>): Promise<void> {
    const docRef = doc(db, 'todos', id);
    const updateData = { ...updates };
    
    if (updates.dueDate) {
      updateData.dueDate = Timestamp.fromDate(updates.dueDate) as any;
    }
    
    await updateDoc(docRef, updateData);
  },

  async delete(id: string): Promise<void> {
    const docRef = doc(db, 'todos', id);
    await deleteDoc(docRef);
  },

  // Suscripción en tiempo real
  subscribe(userId: string, callback: (todos: Todo[]) => void) {
    const q = query(
      todosCollection,
      where('userId', '==', userId),
      orderBy('createdAt', 'desc')
    );

    return onSnapshot(q, (snapshot) => {
      const todos = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data(),
        createdAt: doc.data().createdAt?.toDate(),
        dueDate: doc.data().dueDate?.toDate(),
      })) as Todo[];
      
      callback(todos);
    });
  },
};

Hook con Firestore y Tiempo Real

// hooks/useFirestoreTodos.ts
import { useState, useEffect } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { firestoreTodos } from '@/lib/firestore-db';

export function useFirestoreTodos(userId: string) {
  const queryClient = useQueryClient();
  const [todos, setTodos] = useState<Todo[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  // Suscripción en tiempo real
  useEffect(() => {
    const unsubscribe = firestoreTodos.subscribe(userId, (newTodos) => {
      setTodos(newTodos);
      setIsLoading(false);
      queryClient.setQueryData(['todos', userId], newTodos);
    });

    return () => unsubscribe();
  }, [userId, queryClient]);

  const createMutation = useMutation({
    mutationFn: (data: { title: string; description?: string }) =>
      firestoreTodos.create({
        userId,
        title: data.title,
        description: data.description,
        completed: false,
      }),
  });

  const updateMutation = useMutation({
    mutationFn: ({ id, updates }: { id: string; updates: Partial<Todo> }) =>
      firestoreTodos.update(id, updates),
  });

  const deleteMutation = useMutation({
    mutationFn: (id: string) => firestoreTodos.delete(id),
  });

  return {
    todos,
    isLoading,
    create: createMutation.mutateAsync,
    update: updateMutation.mutateAsync,
    delete: deleteMutation.mutateAsync,
  };
}

Backend Propio con Prisma

Estructura

backend/
├── prisma/
│   └── schema.prisma
├── trpc/
│   ├── todos/
│   │   ├── list/route.ts
│   │   ├── create/route.ts
│   │   ├── update/route.ts
│   │   └── delete/route.ts
│   └── app-router.ts
└── lib/
    └── prisma.ts

Schema de Prisma

// backend/prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  avatar    String?
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  
  todos     Todo[]
  sessions  Session[]
}

model Session {
  id        String   @id @default(cuid())
  userId    String
  token     String   @unique
  expiresAt DateTime
  createdAt DateTime @default(now())
  
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Todo {
  id          String    @id @default(cuid())
  userId      String
  title       String
  description String?
  completed   Boolean   @default(false)
  dueDate     DateTime?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  
  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  
  @@index([userId])
}

Procedimientos tRPC

// backend/trpc/todos/list/route.ts
import { z } from 'zod';
import { protectedProcedure } from '../../trpc';

export const listTodosProcedure = protectedProcedure
  .input(z.object({
    completed: z.boolean().optional(),
    limit: z.number().min(1).max(100).default(50),
    cursor: z.string().optional(),
  }).optional())
  .query(async ({ ctx, input }) => {
    const { completed, limit, cursor } = input || {};

    const todos = await ctx.db.todo.findMany({
      where: {
        userId: ctx.userId,
        ...(completed !== undefined && { completed }),
      },
      take: limit + 1,
      cursor: cursor ? { id: cursor } : undefined,
      orderBy: { createdAt: 'desc' },
    });

    let nextCursor: string | undefined;
    if (todos.length > limit) {
      const nextItem = todos.pop();
      nextCursor = nextItem?.id;
    }

    return {
      items: todos,
      nextCursor,
    };
  });
// backend/trpc/todos/create/route.ts
import { z } from 'zod';
import { protectedProcedure } from '../../trpc';

export const createTodoProcedure = protectedProcedure
  .input(z.object({
    title: z.string().min(1).max(200),
    description: z.string().max(1000).optional(),
    dueDate: z.string().datetime().optional(),
  }))
  .mutation(async ({ ctx, input }) => {
    const todo = await ctx.db.todo.create({
      data: {
        userId: ctx.userId,
        title: input.title,
        description: input.description,
        dueDate: input.dueDate ? new Date(input.dueDate) : null,
      },
    });

    return todo;
  });

Uso en el Cliente

// En componentes React
import { trpc } from '@/lib/trpc';

function TodoList() {
  const todosQuery = trpc.todos.list.useQuery({ limit: 20 });
  const createMutation = trpc.todos.create.useMutation({
    onSuccess: () => {
      todosQuery.refetch();
    },
  });

  const handleCreate = () => {
    createMutation.mutate({
      title: 'Nuevo todo',
      description: 'Descripción opcional',
    });
  };

  if (todosQuery.isLoading) return <Loading />;
  if (todosQuery.error) return <Error message={todosQuery.error.message} />;

  return (
    <FlatList
      data={todosQuery.data?.items}
      renderItem={({ item }) => <TodoItem todo={item} />}
    />
  );
}

Patrones de Sincronización

Offline-First con Cola de Sincronización

// lib/syncQueue.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';

interface SyncOperation {
  id: string;
  type: 'create' | 'update' | 'delete';
  table: string;
  data: any;
  timestamp: number;
}

const QUEUE_KEY = 'sync_queue';

export const syncQueue = {
  async add(operation: Omit<SyncOperation, 'id' | 'timestamp'>) {
    const queue = await this.getQueue();
    queue.push({
      ...operation,
      id: Date.now().toString(),
      timestamp: Date.now(),
    });
    await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
  },

  async getQueue(): Promise<SyncOperation[]> {
    const stored = await AsyncStorage.getItem(QUEUE_KEY);
    return stored ? JSON.parse(stored) : [];
  },

  async remove(id: string) {
    const queue = await this.getQueue();
    const updated = queue.filter(op => op.id !== id);
    await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(updated));
  },

  async processQueue(syncFn: (op: SyncOperation) => Promise<void>) {
    const isConnected = await NetInfo.fetch().then(state => state.isConnected);
    if (!isConnected) return;

    const queue = await this.getQueue();
    
    for (const operation of queue) {
      try {
        await syncFn(operation);
        await this.remove(operation.id);
      } catch (error) {
        console.error('Sync failed for operation:', operation.id, error);
        break; // Stop processing on error to maintain order
      }
    }
  },
};

Hook de Sincronización

// hooks/useSyncedData.ts
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';
import { syncQueue } from '@/lib/syncQueue';

export function useSyncedData() {
  const queryClient = useQueryClient();

  useEffect(() => {
    // Escuchar cambios de conectividad
    const unsubscribe = NetInfo.addEventListener(state => {
      if (state.isConnected) {
        // Procesar cola cuando hay conexión
        syncQueue.processQueue(async (operation) => {
          // Implementar lógica de sincronización según el tipo
          switch (operation.type) {
            case 'create':
              await api.create(operation.table, operation.data);
              break;
            case 'update':
              await api.update(operation.table, operation.data.id, operation.data);
              break;
            case 'delete':
              await api.delete(operation.table, operation.data.id);
              break;
          }
        });

        // Refetch datos después de sincronizar
        queryClient.invalidateQueries();
      }
    });

    return () => unsubscribe();
  }, [queryClient]);
}

Migraciones y Esquemas

Supabase Migrations

-- supabase/migrations/001_initial.sql
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE todos (
  id SERIAL PRIMARY KEY,
  user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  description TEXT,
  completed BOOLEAN DEFAULT FALSE,
  due_date TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Índices
CREATE INDEX idx_todos_user_id ON todos(user_id);

-- RLS (Row Level Security)
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

-- Políticas
CREATE POLICY "Users can view own profile"
  ON profiles FOR SELECT
  USING (auth.uid() = id);

CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  USING (auth.uid() = id);

CREATE POLICY "Users can view own todos"
  ON todos FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can create own todos"
  ON todos FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own todos"
  ON todos FOR UPDATE
  USING (auth.uid() = user_id);

CREATE POLICY "Users can delete own todos"
  ON todos FOR DELETE
  USING (auth.uid() = user_id);

Checklist de Implementación

  • Elegir solución de base de datos según necesidades
  • Configurar variables de entorno
  • Definir esquema/tipos de datos
  • Implementar operaciones CRUD básicas
  • Integrar con React Query para caché
  • Implementar manejo de errores
  • Agregar estados de carga en UI
  • Implementar tiempo real (si aplica)
  • Configurar sincronización offline (si aplica)
  • Probar en iOS, Android y Web
  • Implementar migraciones para cambios de esquema