De MVP a Producción: Supabase en el mundo real
Kevin Dávila
El día que tu MVP aparece en Product Hunt sin avisar descubres qué partes de tu stack eran de juguete. Las conexiones a la base de datos se agotan, la query que funcionaba con 10 usuarios se traba con 1.000, y ese RLS que "así estaba bien" se convierte en tu peor pesadilla. Todo al mismo tiempo, con tráfico real.
Lo mejor: casi todo se corrige antes del lanzamiento. En este post tienes las lecciones de llevar Supabase a producción de verdad: connection pooling, índices, seguridad y escalado.
Connection pooling
Supabase limita las conexiones simultáneas a PostgreSQL. Para producción, necesitas pooling.
Usar Supavisor
Supabase incluye Supavisor, su pooler de conexiones. Configura tu cliente para usarlo:
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
// Para queries normales (usa el pooler)
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// Para operaciones que necesitan conexiones directas (migraciones, etc.)
export const supabaseDirect = createClient(
process.env.SUPABASE_URL!.replace('.supabase.co', '-pooler.supabase.co'),
process.env.SUPABASE_ANON_KEY!
)Configuración en el dashboard
Ve a Settings > Database > Connection pooling:
Mode: Transaction (recomendado para la mayoría de casos)
Pool size: 15-25 (free tier), 50+ (pro)
Default timeout: 10 segundos
Performance
Índices
Crea índices en columnas que filtras frecuentemente:
-- Índice en user_id (muy común)
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
-- Índice compuesto para queries comunes
CREATE INDEX idx_tasks_user_completed ON tasks(user_id, completed);
-- Índice para ordenamiento por fecha
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);Analizar queries lentas
-- Ver queries lentas
SELECT * FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Analizar una query específica
EXPLAIN ANALYZE
SELECT * FROM tasks
WHERE user_id = 'uuid'
AND completed = false
ORDER BY created_at DESC;Pagination
No traigas todos los datos de una vez:
// src/pagination.ts
const PAGE_SIZE = 20
export async function getTasks(page: number = 1) {
const from = (page - 1) * PAGE_SIZE
const to = from + PAGE_SIZE - 1
const { data, error, count } = await supabase
.from('tasks')
.select('*', { count: 'exact' })
.range(from, to)
.order('created_at', { ascending: false })
return {
data,
total: count,
page,
totalPages: Math.ceil((count || 0) / PAGE_SIZE)
}
}Seguridad en producción
Revisar políticas de RLS
Antes de ir a producción, audita tus políticas:
-- Ver todas las políticas
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';Service Role Key
Nunca expongas la service role key en el cliente. Guárdala en variables de entorno del servidor:
// src/lib/supabase-admin.ts (SOLO SERVIDOR)
import { createClient } from '@supabase/supabase-js'
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)Rate limiting
Supabase no tiene rate limiting built-in. Implementa en tu Edge Function o API:
// supabase/functions/api/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
const rateLimit = new Map<string, { count: number; reset: number }>()
serve(async (req) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const limit = rateLimit.get(ip)
if (limit && limit.count > 100 && now < limit.reset) {
return new Response('Rate limit exceeded', { status: 429 })
}
if (!limit || now > limit.reset) {
rateLimit.set(ip, { count: 1, reset: now + 60000 })
} else {
limit.count++
}
// Tu lógica aquí
})Migraciones
Supabase CLI
// Crear migración
supabase migration new add_tasks_table
// Aplicar migraciones localmente
supabase db reset
// Aplicar en producción
supabase db pushEstructura de migraciones
supabase/
migrations/
20240101000000_create_users.sql
20240102000000_create_tasks.sql
20240103000000_add_rls_policies.sqlEjemplo de migración
-- supabase/migrations/20240102000000_create_tasks.sql
CREATE TABLE tasks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Habilitar RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Políticas
CREATE POLICY "Users can view own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own tasks" ON tasks FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own tasks" ON tasks FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own tasks" ON tasks FOR DELETE USING (auth.uid() = user_id);
-- Índices
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);Monitoring
Logs en el dashboard
Ve a Logs para ver:
API logs
Auth logs
Database logs
Edge Function logs
Métricas importantes
Monitorea:
Conexiones activas: No debe llegar al límite
Query duration: Queries > 100ms necesitan optimización
Error rate: Aumento indica problemas
Storage usage: No exceder el plan
Alertas
Configura alertas en tu proveedor de hosting (Vercel, Netlify, etc.) para:
Errores 500
Latencia alta
Caídas del servicio
Backups
Supabase backups
Free tier: Backups diarios, retención de 7 días
Pro: Backups diarios, retención de 28 días
Team/Enterprise: Point-in-time recovery
Backup manual
// Backup de la base de datos
supabase db dump > backup.sql
// Restaurar
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sqlCostos
Free tier limits
Recurso | Límite |
|---|---|
Database | 500 MB |
Storage | 1 GB |
Auth users | 50,000 |
Bandwidth | 500 MB |
Edge Functions | 500K invocations |
Optimizar costos
Database: Usa índices, optimiza queries, archiva datos viejos
Storage: Comprime imágenes, usa CDN, elimina archivos no usados
Bandwidth: Usa transformaciones de imágenes, cachea respuestas
Edge Functions: Reduce invocations, usa batch operations
Cuándo upgrade
Pro ($25/mes): Cuando necesitas más de 500MB de DB o backups mejorados
Team ($599/mes): Cuando necesitas SOC2, HIPAA, o soporte prioritario
Checklist de producción
Antes de lanzar:
[ ] RLS habilitado en todas las tablas
[ ] Service role key solo en el servidor
[ ] Índices en columnas frecuentemente consultadas
[ ] Connection pooling configurado
[ ] Migraciones versionadas
[ ] Backups configurados
[ ] Monitoreo activo
[ ] Rate limiting implementado
[ ] Error handling sólido
[ ] Logs centralizados
Qué sigue
En el próximo (y último) artículo vamos con Supabase para Mobile: cómo usar Supabase con Flutter y React Native para apps nativas.