Codea Bien Logo
De MVP a Producción: Supabase en el mundo real
Supabase

De MVP a Producción: Supabase en el mundo real

Kevin Dávila

supabase productionsupabase scalingsupabase best practicesdatabase optimizationsupabase security

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 push

Estructura de migraciones

supabase/
  migrations/
    20240101000000_create_users.sql
    20240102000000_create_tasks.sql
    20240103000000_add_rls_policies.sql

Ejemplo 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.sql

Costos

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

  1. Database: Usa índices, optimiza queries, archiva datos viejos

  2. Storage: Comprime imágenes, usa CDN, elimina archivos no usados

  3. Bandwidth: Usa transformaciones de imágenes, cachea respuestas

  4. 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.