Checklist de Producción para Supabase

Tener tu app funcionando en desarrollo no significa que está lista para producción. Supabase te da muchas cosas gratis, pero hay configuraciones que vienen desactivadas por defecto y que necesitas activar antes de que usuarios reales toquen tu proyecto.

Este checklist cubre todo lo que tienes que revisar. No es opcional -- saltarte cualquiera de estos pasos puede resultar en datos expuestos, rendimiento degradado o costos inesperados.

Seguridad de la base de datos

Activar RLS en todas las tablas

RLS (Row Level Security -- seguridad a nivel de fila) es el mecanismo de Supabase para controlar quién puede leer o escribir cada fila de tu base de datos. Por defecto, las tablas nuevas no tienen RLS activado, lo que significa que cualquier persona con tu anon key puede leer todo.

sql
-- Verificar que tablas NO tienen RLS activado
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
  AND rowsecurity = false;

Si esa query devuelve resultados, tienes tablas expuestas. Activa RLS en cada una:

sql
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
Una tabla sin RLS es una tabla pública

Si una tabla tiene RLS desactivado, cualquier request autenticado con la anon key puede hacer SELECT, INSERT, UPDATE y DELETE sin restricciones. Esto incluye bots, scrapers y cualquier persona que inspeccione tu frontend.

Revisar todas las políticas de RLS

Activar RLS sin crear políticas (policies -- las reglas que definen quién puede hacer qué) bloquea todo el acceso. Eso está bien como punto de partida, pero necesitas políticas correctas para que tu app funcione.

sql
-- Listar todas las politicas activas
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;

Revisa cada política con estas preguntas:

  • ¿SELECT: Solo los usuarios correctos pueden leer los datos?
  • ¿INSERT: Solo usuarios autenticados pueden crear registros?
  • ¿UPDATE: Los usuarios solo pueden modificar sus propios datos?
  • ¿DELETE: Quién puede borrar y bajo qué condiciones?

Ejemplo de políticas bien definidas para una tabla de perfiles:

sql
-- Los usuarios solo pueden leer su propio perfil
CREATE POLICY "Usuarios leen su perfil"
  ON public.profiles
  FOR SELECT
  USING (auth.uid() = user_id);
 
-- Los usuarios solo pueden actualizar su propio perfil
CREATE POLICY "Usuarios actualizan su perfil"
  ON public.profiles
  FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);
 
-- Solo el backend (service_role) puede eliminar perfiles
CREATE POLICY "Solo service_role elimina perfiles"
  ON public.profiles
  FOR DELETE
  USING (auth.role() = 'service_role');
Cuidado con las políticas permisivas

Una política con USING (true) es equivalente a no tener RLS. Si necesitas acceso público a datos, sé específico: limita las columnas con una vista en lugar de abrir toda la tabla.

Revisar el uso de API keys

Tu proyecto de Supabase tiene dos API keys:

  • anon key: pública, se incluye en el frontend. Toda operación pasa por RLS.
  • service_role key: privada, tiene acceso total a la base de datos. Bypasea RLS por completo.
typescript
// CORRECTO: anon key en el cliente
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
 
// CORRECTO: service_role solo en el servidor
// Nunca importes esto en un componente de cliente
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
Nunca expongas la service_role key

Si la service_role key aparece en tu bundle de JavaScript del cliente, cualquier persona puede leer, escribir y borrar toda tu base de datos. Verifica que no tenga el prefijo NEXT_PUBLIC_ y que solo se use en Server Components, Server Actions o Route Handlers.

Autenticación

Activar confirmación por email

Por defecto, Supabase permite que los usuarios se registren sin confirmar su email. En producción, esto es inaceptable.

  1. Ve a Authentication > Settings en el dashboard
  2. Activa Enable email confirmations
  3. Personaliza el template del email de confirmación con tu branding

Configurar SMTP personalizado

Los emails que envía Supabase por defecto (confirmación, reset de password, magic links) usan un servicio compartido con rate limits (límites de envío) bajos. En producción necesitas tu propio SMTP.

  1. En el dashboard, ve a Settings > Auth > SMTP Settings
  2. Configura tu proveedor (Resend, Postmark, SendGrid, Amazon SES)
  3. Verifica que los emails lleguen y no caigan en spam
plaintext
Sender name: Tu App
Sender email: noreply@tudominio.com
Host: smtp.resend.com
Port: 465
Username: resend
Password: (tu API key del proveedor)
Resend es una buena opción para empezar

Resend tiene un free tier generoso (100 emails/día) y se configura en minutos. Para la mayoría de apps en etapa temprana, es más que suficiente.

Rate Limiting

Supabase incluye rate limiting a nivel de la API gateway, pero las configuraciones por defecto son generosas. Revisa y ajusta según tu caso de uso:

  1. Ve a Settings > API > Rate Limiting
  2. Configura límites para:
    • Auth endpoints: limita intentos de login para prevenir ataques de fuerza bruta
    • REST API: limita requests por IP
    • Realtime: limita conexiones simultáneas

Para auth, considera agregar rate limiting adicional en tu aplicación:

typescript
// Ejemplo: rate limit basico en un Server Action
import { headers } from 'next/headers';
 
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
 
export async function loginAction(formData: FormData) {
  'use server';
 
  const headersList = await headers();
  const ip = headersList.get('x-forwarded-for') ?? 'unknown';
  const now = Date.now();
  const windowMs = 15 * 60 * 1000; // 15 minutos
  const maxAttempts = 5;
 
  const record = rateLimitMap.get(ip);
 
  if (record && now - record.timestamp < windowMs && record.count >= maxAttempts) {
    throw new Error('Demasiados intentos. Intenta de nuevo en 15 minutos.');
  }
 
  if (!record || now - record.timestamp >= windowMs) {
    rateLimitMap.set(ip, { count: 1, timestamp: now });
  } else {
    record.count++;
  }
 
  // ... logica de login
}

Backups y Recuperación

Activar Point-in-Time Recovery (PITR)

PITR (Point-in-Time Recovery -- recuperación a un punto exacto en el tiempo) te permite restaurar tu base de datos al estado que tenía en cualquier momento de los últimos días. Es diferente a un backup diario porque no perderás las horas entre el último backup y el momento del problema.

  • Disponible en el plan Pro y superiores
  • Se activa en Settings > Database > Backups > Point-in-Time Recovery
  • Permite restaurar a cualquier segundo dentro del periodo de retención
PITR requiere el plan Pro

El free tier solo incluye backups diarios automáticos con retención de 7 días. Si tu app maneja datos críticos (pagos, datos de usuarios, órdenes), el upgrade a Pro es necesario solo por esta feature.

Backups automáticos

Independientemente de PITR, verifica que los backups automáticos estén funcionando:

  1. Ve a Settings > Database > Backups
  2. Confirma que ves backups recientes en la lista
  3. Descarga uno y verifícalo -- un backup que nunca probaste no es un backup

SSL y Conexiones

Forzar SSL

Todas las conexiones a tu base de datos deben usar SSL (Secure Sockets Layer -- conexión encriptada). Supabase lo activa por defecto para conexiones via el SDK, pero si te conectas directamente con un cliente PostgreSQL, verifica que SSL esté forzado.

En el dashboard: Settings > Database > SSL Enforcement > Enable

Connection Pooling con PgBouncer

Cada conexión a PostgreSQL consume memoria del servidor. En producción, tu app puede abrir cientos de conexiones simultáneas, lo que puede degradar el rendimiento o directamente tirar la base de datos.

PgBouncer (un connection pooler -- administrador de conexiones que recicla las existentes en lugar de crear nuevas) resuelve esto.

Supabase incluye PgBouncer integrado. Para usarlo:

  1. Ve a Settings > Database > Connection Pooling
  2. Activa el pooler
  3. Usa la connection string del pooler en lugar de la conexión directa
plaintext
# Conexion directa (NO uses en produccion para la app)
postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres
 
# Connection pooler (usa esta)
postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
typescript
// Para Prisma o Drizzle, usa la URL del pooler
// en tu .env
// DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true
Modos de PgBouncer

Supabase ofrece dos modos: Transaction (por defecto, recomendado para la mayoría de apps) y Session. Si usas PREPARE statements o LISTEN/NOTIFY, necesitas el modo Session. Para todo lo demás, Transaction mode es la opción correcta.

Performance

Índices

Los índices aceleran las queries evitando que PostgreSQL tenga que escanear toda la tabla. En producción, la falta de índices es la causa número uno de queries lentas.

sql
-- Crear un indice en columnas que usas frecuentemente en WHERE o JOIN
CREATE INDEX idx_orders_user_id ON public.orders (user_id);
CREATE INDEX idx_orders_created_at ON public.orders (created_at DESC);
 
-- Indice compuesto para queries que filtran por múltiples columnas
CREATE INDEX idx_orders_user_status ON public.orders (user_id, status);

Para encontrar queries lentas, usa el dashboard de Supabase:

  1. Ve a Database > Query Performance
  2. Ordena por tiempo de ejecución
  3. Las queries que aparecen arriba son las que necesitan índices
sql
-- Verificar si un indice se esta usando
EXPLAIN ANALYZE
SELECT * FROM public.orders WHERE user_id = 'uuid-del-usuario';

Si ves Seq Scan en lugar de Index Scan, tu índice no se está usando o no existe.

Optimizar queries

Algunas reglas prácticas:

  • Selecciona solo las columnas que necesitas, no uses SELECT *
  • Usa paginación con .range() en vez de traer todos los registros
  • Evita N+1 queries -- usa relaciones en una sola query con .select('*, author(*)')
typescript
// MAL: trae todo
const { data } = await supabase.from('products').select('*');
 
// BIEN: solo lo que necesitas, paginado
const { data } = await supabase
  .from('products')
  .select('id, name, price, image_url')
  .order('created_at', { ascending: false })
  .range(0, 19);

Monitoreo y Alertas

Dashboard de Supabase

El dashboard incluye métricas de uso en Settings > Usage:

  • Requests por endpoint
  • Almacenamiento de base de datos
  • Ancho de banda
  • Usuarios activos

Configurar alertas

No esperes a que tu app se caiga para enterarte. Configura alertas para:

  • Uso de CPU por encima del 80%
  • Almacenamiento acercándose al límite del plan
  • Errores de auth inusuales (posible ataque)
  • Queries lentas por encima de un threshold (umbral)

Puedes integrar con servicios externos como Grafana, Datadog o simplemente webhooks a Slack/Discord.

Plan de Supabase

Cuándo hacer upgrade al plan Pro

El free tier de Supabase es generoso para desarrollo y MVPs, pero para producción real necesitas evaluar:

FeatureFreePro
Base de datos500 MB8 GB (expandible)
Bandwidth5 GB250 GB
Storage1 GB100 GB
Edge Functions500K invocaciones2M invocaciones
BackupsDiarios, 7 díasPITR, 30 días
SoporteComunidadEmail
No esperes a que el free tier se quede corto

Si ya tienes usuarios reales pagando por tu producto, el costo del plan Pro ($25/mes por proyecto) es insignificante comparado con el riesgo de downtime o pérdida de datos. Haz el upgrade antes de necesitarlo.

Checklist resumido

Antes de lanzar, recorre cada punto:

  • RLS activado en todas las tablas
  • Políticas de RLS revisadas y testeadas
  • Confirmación de email activada
  • SMTP personalizado configurado
  • Rate limiting configurado en auth y API
  • Backups verificados (descargaste y probaste uno)
  • PITR activado (si tu plan lo permite)
  • SSL forzado en todas las conexiones
  • Connection pooling activado
  • Índices creados para queries frecuentes
  • service_role key solo en el servidor
  • Monitoreo y alertas configurados
  • Plan evaluado según las necesidades de tu app