🗄️ Database Guide
Index
Database Options
Local Storage (AsyncStorage)
Local SQLite
Supabase (PostgreSQL)
Firebase Firestore
Custom Backend with Prisma
Sync Patterns
Migrations and Schemas
Database Options
Option Type Best for Pros Cons AsyncStorage Local (Key-Value) Preferences, simple cache Simple, included Not relational, size limit SQLite Local (SQL) Offline-first apps, structured data Relational, fast, offline Local only, no sync Supabase Cloud (PostgreSQL) Apps with users, real-time SQL, built-in Auth, real-time Requires connection Firebase Firestore Cloud (NoSQL) Simple apps, real-time Easy setup, scalable Limited queries, costly at scale Backend + Prisma Cloud (SQL) Full control, complex apps Flexible, typed More 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