🗄️ 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