1. 🔑 Secrets en .env, nunca en el repo
Qué es: Las API keys de OpenAI, Anthropic, Stripe, Supabase, Resend — todo lo que tenga una cadena tipo sk-..., pk_..., eyJ... — va en un archivo .env local y en las variables de entorno de tu plataforma. Nunca en el código.
Por qué importa: Si pusheas una API key al repo público, hay bots que escanean GitHub cada minuto. Tu key dura entre 30 segundos y 5 minutos antes de que alguien la use para minar cripto o quemarte la cuota.
Cómo se ve hecho:
.env está en .gitignore
- Hay un
.env.example con los nombres de las variables, sin valores
- Si ya pusheaste una key: rótala en el dashboard del proveedor. Borrar el commit no la limpia.
Verifica: corre git log -p | grep -iE "sk-|api_key|secret" en tu repo. Si aparece algo, hay key expuesta en el historial.
2. ⚙️ Env vars cargadas en producción
Qué es: Las mismas variables que tienes en .env local tienen que estar pegadas en el dashboard de Vercel, Railway, Netlify o donde sea que deployes.
Por qué importa: El error más común post-deploy: "funciona en local pero en producción truena". 8 de cada 10 veces son env vars que olvidaste configurar en el dashboard, o que copiaste con un espacio extra.
Cómo se ve hecho:
- Cada variable de
.env.example está creada en producción
- Las variables sensibles están marcadas como "secret" en la plataforma (Vercel lo hace por defecto)
- Si tienes ambientes preview/staging, las env vars también están ahí — no solo en production
- En Next.js, las que se usan en el cliente empiezan con
NEXT_PUBLIC_
Verifica: abre la consola del navegador en producción y revisa que no haya errores tipo undefined is not a function o Cannot read property X of undefined por env vars faltantes.
3. 💸 Spending limits en APIs de IA
Qué es: Un límite máximo de gasto mensual en OpenAI, Anthropic, Replicate, ElevenLabs, o cualquier API que cobre por uso.
Por qué importa: Un bug en un loop, un usuario malicioso, o un agente que se enreda en sí mismo, te pueden generar $3,000 en una madrugada. Sin límite, la tarjeta se cobra. Con límite, la API responde con error y tú duermes.
Cómo se ve hecho:
- En OpenAI: Settings → Limits → "Monthly budget" + "Email alert threshold"
- En Anthropic Console: Settings → Plans & billing → spend limits
- En Stripe (si cobras): tienes alertas de gastos inusuales activadas
- Las alertas llegan a un email que sí revisas, no a uno que ignoras
Verifica: revisa que tengas un límite numérico, no solo "alertas". El límite tiene que cortar el servicio, no solo avisarte.
4. 🔒 Auth + autorización en cada endpoint
Qué es: Dos cosas distintas:
- Autenticación: el endpoint sabe quién está llamando (token, sesión)
- Autorización: el endpoint sabe si esa persona puede hacer esa acción
Por qué importa: El frontend esconde el botón de "borrar usuario", pero el endpoint DELETE /api/users/123 está abierto a cualquiera que lo descubra. Otro clásico: el user_id viene del frontend en lugar de leerse del token de sesión, así que cualquiera edita los datos de cualquiera cambiando un número en la URL.
Cómo se ve hecho:
- Cada endpoint del backend tiene middleware de auth, no solo las vistas del frontend
- El
user_id se lee siempre del token/sesión del servidor, nunca del request body o de la URL
- Cada query a la base de datos filtra por el
user_id autenticado: where user_id = session.user.id
- Si usas Supabase: Row Level Security (RLS) está activado en cada tabla
- Hay roles separados (
admin, user) y se validan en el backend
Verifica: abre dos cuentas de prueba. Logueado como usuario A, intenta acceder a un recurso de usuario B cambiando el ID en la URL o en el request. Si funciona, hay agujero.
5. 🛡️ Inputs validados + queries parametrizadas
Qué es: Toda data que viene del usuario (formularios, URL params, headers) se valida con un schema antes de tocar la lógica. Y toda query a la DB usa parámetros, no concatenación de strings.
Por qué importa: Sin validación, un email puede ser 10KB de texto malicioso. Sin queries parametrizadas, alguien escribe '; DROP TABLE users;-- y te borra la base de datos. SQL injection sigue siendo el top 1 de OWASP por algo.
Cómo se ve hecho:
- Validación con Zod, Yup, o Valibot en cada endpoint (no solo en el frontend)
- ORMs como Prisma, Drizzle, o el SDK de Supabase — todos parametrizan por defecto. Si escribes SQL crudo, usa placeholders (
$1, ?), nunca template strings con datos del usuario
- Outputs del usuario que se muestran a otros usuarios pasan por escape de HTML (React lo hace por defecto, pero
dangerouslySetInnerHTML lo rompe)
- Límite de tamaño en cada campo (longitud máxima de strings, tamaño máximo de uploads)
Verifica: intenta enviar un campo name con 10,000 caracteres, un email sin @, un número negativo donde debería ser positivo. ¿La API responde con error 400, o lo acepta?
6. ⏳ Reset links caducables (15-60 min)
Qué es: Los enlaces de "olvidé mi contraseña", verificación de email, o invitaciones, expiran después de un tiempo corto.
Por qué importa: Un link de reset que dura para siempre es una llave maestra. Si el correo del usuario se compromete meses después, ese link sigue siendo válido.
Cómo se ve hecho:
- Reset de contraseña: 15-60 minutos
- Verificación de email: 24 horas máximo
- Invitaciones a equipo: 7 días, o un solo uso
- El token es de un solo uso: después de usarlo, se invalida
- Si usas Supabase Auth, Clerk, o Auth0, esto viene configurado — verifica el valor por defecto en el dashboard
Verifica: pide un reset, espera el tiempo de expiración + 5 minutos, intenta usar el link. Debe responder "link expirado", no permitir el reset.
7. 🚦 Rate limiting por IP y por usuario
Qué es: Un máximo de requests por minuto/hora por IP, por usuario, y por endpoint sensible.
Por qué importa: Sin rate limiting, alguien puede:
- Probar 10,000 contraseñas por segundo en tu login (brute force)
- Pedir 5,000 resets de contraseña por minuto y saturar tu cuota de Resend/SendGrid
- Llamar tu endpoint que usa OpenAI 50 veces por segundo y quemarte la cuenta
Cómo se ve hecho:
- Login, signup, reset password, forgot password: 5-10 intentos por hora por IP
- Endpoints que usan APIs pagas: 10-30 por minuto por usuario
- Endpoints públicos: 60-100 por minuto por IP
- Herramientas: Upstash Rate Limit (gratis para empezar), Vercel Rate Limit, o middleware propio con Redis
Verifica: abre una terminal y corre for i in {1..20}; do curl https://tu-app.com/api/login; done. Después del intento 5-10 debes ver respuestas 429 "Too Many Requests".
8. 🪝 Webhooks verificados con firma
Qué es: Cuando Stripe, Clerk, GitHub o cualquier servicio te manda un webhook, llega con un header de firma. Tu endpoint debe verificar esa firma antes de procesar el evento.
Por qué importa: Tu endpoint de webhook es público — cualquiera puede llamarlo. Sin verificación de firma, alguien puede mandarte un POST falso simulando "Stripe dice que este usuario pagó $1,000" y darle acceso premium sin haber cobrado nada.
Cómo se ve hecho:
- Stripe: usar
stripe.webhooks.constructEvent() con el STRIPE_WEBHOOK_SECRET
- Clerk: usar
@clerk/nextjs con el WEBHOOK_SECRET
- GitHub: verificar el header
x-hub-signature-256 con HMAC
- Nunca confiar en el body del webhook sin verificar la firma primero
- El webhook secret está en env vars, igual que el resto
Verifica: intenta llamar tu endpoint de webhook con curl y un body falso. Debe responder con error 400 o 401, no procesar el evento.
9. 📊 Logs + Sentry + alertas activas
Qué es: Tres capas distintas:
- Logs: registros de qué pasó (consultables después)
- Error tracking (Sentry): te avisa cuándo algo falla, con stack trace y contexto
- Alertas: notificación activa (email, Slack, SMS) cuando algo crítico se rompe
Por qué importa: Sin esto, te enteras de los bugs por el tweet enojado del usuario. Con esto, te enteras en 60 segundos y arreglas antes de que se note.
Cómo se ve hecho:
- Sentry configurado en frontend y backend (free tier alcanza para empezar)
- Logs estructurados (JSON) en lugar de
console.log sueltos
- Alertas configuradas para: error rate > X%, latencia > Y ms, downtime, webhooks que fallan
- Los errores no exponen información sensible en los logs (no loguees passwords, tokens, números de tarjeta)
Verifica: fuerza un error en producción (un endpoint con un throw new Error()). ¿Te llegó la alerta? ¿En cuánto tiempo? Si no llega, no está configurado bien.
10. 💾 Backup automático de la DB confirmado
Qué es: Tu base de datos se respalda automáticamente, y tú sabes cómo restaurarla.
Por qué importa: Un DELETE FROM users sin WHERE (sí, pasa), una migración mal hecha, un servidor que se corrompe — sin backup, perdiste el negocio. Con backup, pierdes un día.
Cómo se ve hecho:
- Supabase: Daily backups vienen en el plan Pro. En Free tier, no hay backup automático — usa
pg_dump manual o paga el upgrade si tienes data real
- Neon, Railway, PlanetScale: revisa el dashboard, confirma frecuencia (idealmente diario)
- Sabes cómo restaurar un backup. Probaste el proceso al menos una vez en un proyecto sandbox
- Los backups se guardan en una región distinta (o al menos, no en el mismo servidor)
Verifica: entra al dashboard de tu DB, busca "Backups". Si no ves la palabra "automated" o "daily", no estás cubierto.
11. 🧪 Flujo crítico probado en el dominio real
Qué es: Después del deploy, no asumas que funciona porque "abrió la página". Recorre el flujo completo de tu producto en el dominio de producción, como si fueras un usuario nuevo.
Por qué importa: En localhost, Stripe usa modo test, los emails se mandan a tu inbox, y los redirects van a localhost:3000. En producción, todo cambia. El bug aparece en el momento más caro: cuando un usuario real está pagando.
Cómo se ve hecho: abre una ventana de incógnito y:
- Signup con un email real (puedes usar un alias tipo
tu+test@gmail.com)
- Verifica que el email de confirmación llega y el link funciona
- Login con la cuenta nueva
- Recorre el flujo principal (crear, editar, borrar lo que sea tu core)
- Si cobras: paga $1 (o usa Stripe en modo test con la key de prod si es posible)
- Verifica que el webhook se procesa y el acceso se desbloquea
- Logout, login otra vez, verifica que el estado persiste
Verifica: si cualquier paso falla o se siente raro, no lances. Aún si tu app "se ve bien".
12. ⏪ Rollback al deploy anterior en un clic
Qué es: Si el deploy que acabas de hacer rompe algo, puedes volver al anterior en menos de 60 segundos.
Por qué importa: Todo el mundo deploya bugs. La diferencia entre un susto y una crisis es el tiempo que pasa entre "se rompió" y "ya está arreglado". El rollback te da minutos en lugar de horas.
Cómo se ve hecho:
- Vercel/Netlify: cada deploy queda guardado. Vas a Deployments → click en uno anterior → "Promote to Production"
- Railway/Render: los deploys se hacen por commit, así que
git revert + push hace rollback
- Las migraciones de base de datos son reversibles, o tienes un plan para reversarlas a mano
- Sabes dónde está el botón. Lo encontraste antes de necesitarlo
Verifica: abre el dashboard de tu plataforma ahora mismo, busca el deploy anterior, y confirma que sabes cómo promoverlo. Si te toma más de 30 segundos encontrar dónde, no es un "rollback en un clic".
Plantilla de .env.example
Pega esto en la raíz de tu proyecto y adapta a lo que uses:
# App
NEXT_PUBLIC_APP_URL=https://tu-app.com
# Database (Supabase / Neon / etc.)
DATABASE_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Auth (Clerk / Auth0 / etc.)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_WEBHOOK_SECRET=
# AI
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
# Pagos
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# Email
RESEND_API_KEY=
# Observability
SENTRY_DSN=
NEXT_PUBLIC_SENTRY_DSN=
Cómo usar este checklist
Primera vez: imprímelo o pégalo en un Notion. Recorre los 12 puntos uno por uno. Te va a tomar entre 1 y 3 horas la primera vez según el estado de tu proyecto.
Cada deploy nuevo: revisión rápida de los puntos que pueden cambiar (env vars, rollback, smoke test). Toma 10 minutos.
Cuando lances algo nuevo grande: los 12 otra vez. Sin atajos.
La regla de oro
Si no puedes responder "sí, está hecho" a los 12, no es momento de deploy. Es momento de cerrar el laptop y dormir. Vuelve mañana, termina los pendientes, y entonces sí.
El deploy que se rompe a las 2 AM cuesta más que el lanzamiento que se atrasa 24 horas. Siempre.
Hecho por Erik Taveras · Taveras Solutions LLC