Runbooks
Procedimientos operativos. Para cada uno: qué hace, cuándo se ejecuta, comandos exactos, qué verificar al final.
- Deploy de
tcgcards-web(frontend) - Deploy de
tcgcards-api(backend) - Rollback de un deploy
- Restaurar backup de Mongo
- Rotar un secret
- Agregar/quitar admin a un usuario
- Forzar la ejecución de un cron job
- Actualizar/agregar credenciales Mercado Pago
- Investigar un error en producción
- Cambiar dominio
Deploy tcgcards-web (frontend)
Sección titulada «Deploy tcgcards-web (frontend)»Cuándo: cada vez que quieres que un cambio del repo tcgcards-web llegue a tcgcards.cl.
Procedimiento (resumen):
- Push a la rama
staging→ Vercel publica enstaging.tcgcards.cl(automático). Pruebas ahí. - Fusiona la rama del feature →
main(no despliega nada · nuncastaging→main). - GitHub →
tcgcards-web→ Actions → “Deploy Web → PROD (manual)” → Run workflow →main.
Fallback sin Actions: cd /Users/yoel/Documents/Repos/tcgcards-web && vercel --prod.
Cómo verificar:
- El run de “Deploy Web → PROD” en verde; en su log aparece
Aliased https://tcgcards.cl. - Abrir https://tcgcards.cl en incognito → verificar el cambio en producción.
Tiempo total: ~6-7 min (CI + build en Vercel).
Deploy tcgcards-api (backend)
Sección titulada «Deploy tcgcards-api (backend)»Cuándo: cada vez que quieres que un cambio del repo tcgcards-api llegue a producción. Esto no es automático.
Procedimiento (resumen):
- Push a la rama
staging→ GitHub Actions despliegatcgcards-api-staging(automático). Pruebas ahí. - Fusiona la rama del feature →
main(no despliega nada · nuncastaging→main). - GitHub →
tcgcards-api→ Actions → “Deploy API → PROD (manual)” → Run workflow →main. (Ejecuta CI y despliega por--image.)
Fallback sin Actions (build remoto en Cloud Build, igual destino):
cd /Users/yoel/Documents/Repos/tcgcards-apibash scripts/deploy-prod.sh# equivale a: gcloud run deploy tcgcards-api --source . --region us-central1 --project api-cards-prod --quietCómo verificar:
# Status del serviciogcloud run services describe tcgcards-api \ --region us-central1 \ --project api-cards-prod \ --format="value(status.conditions[0].type,status.conditions[0].status,status.latestReadyRevisionName)"# Esperado: "Ready True tcgcards-api-XXXXX-xxx"
# Sanity check endpoint públicocurl -s -o /dev/null -w "%{http_code}\n" \ "https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/health"# Esperado: 200Tiempo total: 5-8 min (build + container push + revision routing).
Rollback de un deploy
Sección titulada «Rollback de un deploy»Web (Vercel)
Sección titulada «Web (Vercel)»- https://vercel.com →
tcgcards-web→ Deployments - Encuentra el deploy anterior bueno.
- Click en los
...→ “Promote to Production”.
Toma ~30 segundos. No requiere CLI.
Backend (Cloud Run)
Sección titulada «Backend (Cloud Run)»# 1. Listar revisiones recientesgcloud run revisions list \ --service tcgcards-api \ --region us-central1 \ --project api-cards-prod \ --limit 5
# 2. Apuntar 100% del tráfico a una revisión específicagcloud run services update-traffic tcgcards-api \ --to-revisions=tcgcards-api-00128-gpd=100 \ --region us-central1 \ --project api-cards-prod(Reemplaza tcgcards-api-00128-gpd por la revisión deseada.)
Cómo verificar: comando describe igual que en deploy.
Restaurar backup de Mongo
Sección titulada «Restaurar backup de Mongo»Cuándo: se borró o corrompió data, o quieres ver/recuperar algo de un backup.
Conceptos en 30 segundos (si nunca hiciste esto)
Sección titulada «Conceptos en 30 segundos (si nunca hiciste esto)»- Backup / dump: una copia completa de la base de datos en un momento dado. Es un solo archivo:
dump-AÑOMESDÍA-HORAMINSEG.archive.gz. La hora es en UTC (~4 h adelante de Chile). - Dónde viven: en Google Cloud Storage, en
gs://tcgcards-mongo-backups/. - Restaurar: tomar ese archivo y cargarlo en una base MongoDB.
- Local vs producción:
- Local = una MongoDB “de juguete” en tu computador. No puede romper nada. Empieza siempre por acá.
- Producción = la base real que usan los usuarios. Tocarla es peligroso.
¿Cuál es tu situación? Elige una
Sección titulada «¿Cuál es tu situación? Elige una»- Situación A — “Solo quiero MIRAR un backup o recuperar algún dato perdido”. → Seguro, no toca producción. Hazlo siempre primero.
- Situación B — “Se borró/corrompió data en PRODUCCIÓN y hay que devolverla”. → Peligroso. Más abajo, con calma.
Paso 0 — Instalar herramientas (una sola vez)
Sección titulada «Paso 0 — Instalar herramientas (una sola vez)»En una terminal (Mac):
brew tap mongodb/brewbrew install mongodb-database-tools mongodb-community mongoshbrew install --cask google-cloud-sdk # solo si aún no tienes gcloudVerifica que quedaron instaladas (cada comando debe imprimir una versión, no un error):
mongorestore --versionmongosh --versiongcloud --versionPaso 0b — Conectarte a Google Cloud (una sola vez)
Sección titulada «Paso 0b — Conectarte a Google Cloud (una sola vez)»gcloud auth login # abre el navegador; inicia sesión con yoeldts2@gmail.comgcloud config set project api-cards-prodgcloud auth list # debe mostrar tu correo con un "*" al ladoSi gcloud auth list no muestra tu correo como activo, repite gcloud auth login.
Situación A — Restaurar en un Mongo local (no puedes romper nada)
Sección titulada «Situación A — Restaurar en un Mongo local (no puedes romper nada)»A1. Ver los backups disponibles y elegir uno
gcloud storage ls gs://tcgcards-mongo-backups/ | tail -20Verás líneas como gs://tcgcards-mongo-backups/dump-20260529-211811.archive.gz. El 20260529-211811 es la fecha/hora UTC (29-may-2026, 21:18:11 UTC). Elige el más reciente de ANTES de que ocurriera el problema. Para usar el último de todos:
ULTIMO=$(gcloud storage ls gs://tcgcards-mongo-backups/ | sort | tail -1)echo "Voy a usar: $ULTIMO"A2. Descargarlo a tu computador
mkdir -p /tmp/restoregcloud storage cp "$ULTIMO" /tmp/restore/dump.archive.gz(Si elegiste uno específico, reemplaza "$ULTIMO" por la ruta completa gs://tcgcards-mongo-backups/dump-FECHA.archive.gz.)
A3. Confirmar que el archivo no está corrupto
gzip -t /tmp/restore/dump.archive.gz && echo "OK: archivo íntegro"Debe imprimir OK: archivo íntegro. Si da error, se dañó al bajar — bórralo y vuelve a descargar.
A4. Levantar una MongoDB de juguete (Terminal 1)
Abre una terminal y deja este comando corriendo. Va a quedar imprimiendo logs — es normal, NO cierres esta terminal:
mkdir -p /tmp/mongo-juguetemongod --dbpath /tmp/mongo-juguete --port 27018A5. Cargar el backup en la base de juguete (Terminal 2)
Abre otra terminal (deja la anterior corriendo) y ejecuta:
mongorestore --uri="mongodb://localhost:27018" \ --archive=/tmp/restore/dump.archive.gz --gzipQué deberías ver al final: una línea como
207445 document(s) restored successfully. 0 document(s) failed to restore.
Lo importante es 0 document(s) failed.
A6. Mirar los datos
mongosh "mongodb://localhost:27018/api-cards"Ya dentro (verás un prompt >), prueba:
show collections // lista las coleccionesdb.cards.countDocuments() // ~205000db.orders.countDocuments()
// Buscar un documento específico que se haya perdido, por ejemplo una orden:db.orders.findOne({ _id: "order-XXXXXXX" })
// O un usuario por correo:db.users.findOne({ email: "persona@ejemplo.cl" })Para salir de mongosh escribe exit.
A7. Limpiar al terminar
- Vuelve a la Terminal 1 y aprieta Ctrl+C (apaga la base de juguete).
- Borra los temporales:
rm -rf /tmp/mongo-juguete /tmp/restoreListo. No tocaste producción en ningún momento.
Situación B — Restaurar EN producción (PELIGROSO)
Sección titulada «Situación B — Restaurar EN producción (PELIGROSO)»B1. Antes de tocar nada: backup del estado ACTUAL
Esto te da un “punto de retorno” por si la restauración empeora las cosas:
gcloud run jobs execute mongo-backup \ --region=us-central1 --project=api-cards-prod --waitEspera a que termine (exit 0). El estado actual quedó guardado en GCS.
B2. Decide el alcance (clave)
- ¿Solo una colección quedó mal (ej.
listings)? → restaura solo esa (Opción B-mín). Es lo más seguro: no pierdes el resto. - ¿Está toda la base corrupta/borrada? → restauración completa (Opción B-total). Último recurso.
B3. Obtener la conexión a producción y el dump
URI=$(gcloud secrets versions access latest --secret=mongodb-uri --project=api-cards-prod)Asegúrate de tener el dump elegido descargado en /tmp/restore/dump.archive.gz (Pasos A1–A3). Si no, hazlo ahora.
Opción B-mín — Restaurar UNA sola colección (recomendado)
Reemplaza listings por la colección afectada:
mongorestore --uri="$URI" --gzip \ --archive=/tmp/restore/dump.archive.gz \ --nsInclude="api-cards.listings" --drop--nsInclude="api-cards.listings"= restaura solo esa colección.--drop= borra esa colección antes de restaurarla (la deja igual al backup).
Opción B-total — Restaurar TODA la base (último recurso)
mongorestore --uri="$URI" --gzip \ --archive=/tmp/restore/dump.archive.gz --dropB4. Verificar
mongosh "$URI" --quiet --eval 'const d = db.getSiblingDB("api-cards");print("cards: " + d.cards.countDocuments());print("orders: " + d.orders.countDocuments());print("wallet_entries: " + d.wallet_entries.countDocuments());const sys = d.users.findOne({_id:"user-system-tcgcards"});print("system wallet balance: " + (sys ? sys.walletBalance : "NO ENCONTRADO"));'Los números deberían tener sentido (cards ~205000) y el system wallet debe aparecer.
B5. Si quedó peor que antes
Tienes el backup del Paso B1: repite la restauración usando ese archivo (el que generaste en B1) para volver al estado previo.
RPO efectivo: ~1 hora (los backups son cada hora). Drill de restore verificado el 2026-05-29 (restauración completa, 0 documentos fallidos). El usuario de la app no puede dropear bases de datos (least-privilege), un freno extra contra borrados accidentales.
Rotar un secret
Sección titulada «Rotar un secret»Secret en GCP Secret Manager (backend)
Sección titulada «Secret en GCP Secret Manager (backend)»# 1. Generar el nuevo valor (ejemplo para secret aleatorio de 32 bytes hex)NEW_VALUE=$(openssl rand -hex 32)
# 2. Agregar como nueva versiónecho -n "$NEW_VALUE" | gcloud secrets versions add INTERNAL_JWT_SECRET \ --data-file=- \ --project=api-cards-prod
# 3. Verificargcloud secrets versions access latest \ --secret=INTERNAL_JWT_SECRET \ --project=api-cards-prod
# 4. Redeploy del Cloud Run para que apliquecd /Users/yoel/Documents/Repos/tcgcards-apigcloud run deploy tcgcards-api \ --source . \ --region us-central1 \ --project api-cards-prod \ --quietSecret en Vercel (frontend)
Sección titulada «Secret en Vercel (frontend)»# Listarvercel env ls
# Quitar el viejovercel env rm INTERNAL_JWT_SECRET production
# Agregar el nuevo (interactive, pide valor)vercel env add INTERNAL_JWT_SECRET production
# Redeploy para que apliquevercel --prodCambiar valor sensible y mantener compatibilidad
Sección titulada «Cambiar valor sensible y mantener compatibilidad»Para minimizar downtime al rotar secrets compartidos:
- Generar nuevo valor.
- Setear en backend + redeploy.
- Setear en frontend + redeploy.
- (Entre paso 2 y 3 hay ~2 min de incompatibilidad. Hacer en horario de menor tráfico.)
Agregar/quitar admin a un usuario
Sección titulada «Agregar/quitar admin a un usuario»No hay UI para esto. Se hace por Mongo directo.
# Conectarmongosh "<MONGODB_URI>"> use api-cards
# Hacer admin> db.users.updateOne({ username: "ejemplo" }, { $set: { isAdmin: true } })
# Quitar admin> db.users.updateOne({ username: "ejemplo" }, { $set: { isAdmin: false } })
# Verificar> db.users.findOne({ username: "ejemplo" }, { isAdmin: 1, username: 1 })El cambio aplica inmediatamente — el siguiente request del user con sesión activa lo verá como admin. No hace falta reiniciar nada.
Forzar la ejecución de un cron job
Sección titulada «Forzar la ejecución de un cron job»gcloud scheduler jobs run orders-tick \ --project=api-cards-prod \ --location=us-central1Reemplaza orders-tick por el job que quieras. Lista completa:
mongo-backup-hourlyorders-tickorders-awaiting-payment-cleanupwallet-reconcile-dailyrefunds-reconcile-daily
Cómo verificar:
# Ver logs del Cloud Run del último minutogcloud run services logs read tcgcards-api \ --region=us-central1 \ --project=api-cards-prod \ --limit=50Buscar líneas con prefijo cron. (e.g., cron.canceled, cron.completed).
Actualizar/agregar credenciales Mercado Pago
Sección titulada «Actualizar/agregar credenciales Mercado Pago»Cuándo: se necesita rotar MP_ACCESS_TOKEN, el webhook secret, o cuando MP cambia algo del lado de ellos.
# 1. Obtener token nuevo desde el panel MP → Credenciales productivas (o test si testing)# El token es de la forma APP_USR-XXXXXX-XXXXXX-XXXXXX-XXXXXX
# 2. Actualizar el secretecho -n "APP_USR-..." | gcloud secrets versions add MP_ACCESS_TOKEN \ --data-file=- \ --project=api-cards-prod
# 3. Redeploycd /Users/yoel/Documents/Repos/tcgcards-apigcloud run deploy tcgcards-api --source . --region us-central1 --project api-cards-prod --quietPara el MP_WEBHOOK_SECRET:
- En el panel MP, configurar el webhook con el endpoint:
https://tcgcards-api-1033181994095.us-central1.run.app/api/v1/webhooks/mp - MP genera un secret — copiarlo
echo -n "SECRET" | gcloud secrets versions add MP_WEBHOOK_SECRET --data-file=- --project=api-cards-prod- Redeploy.
Investigar un error en producción
Sección titulada «Investigar un error en producción»1. Sentry (errores capturados)
Sección titulada «1. Sentry (errores capturados)»https://sentry.io → proyecto correspondiente (web o api) → buscar el error por mensaje o usuario.
2. Logs estructurados del backend
Sección titulada «2. Logs estructurados del backend»gcloud run services logs read tcgcards-api \ --region=us-central1 \ --project=api-cards-prod \ --limit=100 \ --format=json | jq '.[] | select(.severity == "ERROR")'3. Mongo directo (datos del user/orden afectada)
Sección titulada «3. Mongo directo (datos del user/orden afectada)»mongosh "<MONGODB_URI>"> use api-cards> db.orders.findOne({ _id: "order-xxxxx" })> db.users.findOne({ email: "afectado@example.com" }, { walletBalance: 1, status: 1 })4. PostHog (frontend events)
Sección titulada «4. PostHog (frontend events)»https://app.posthog.com → buscar por user properties (email/username).
Cambiar dominio
Sección titulada «Cambiar dominio»Si alguna vez se cambia el dominio tcgcards.cl a otro:
- DNS en Cloudflare: apuntar el nuevo dominio a Vercel + Cloud Run.
- Vercel: agregar el nuevo dominio en el proyecto
tcgcards-web→ Settings → Domains. - Env vars del web: actualizar
NEXT_PUBLIC_SITE_URL→ redeploy Vercel. - Env vars del api: actualizar
WEB_PUBLIC_URL(afecta los emails que mandamos) → redeploy Cloud Run. - OAuth Google: agregar
https://nuevo-dominio.cl/api/auth/callback/googlea los Authorized Redirect URIs en Google Cloud Console. - MP webhooks: registrar el nuevo URL en el panel MP.
- Resend: verificar el nuevo dominio (SPF + DKIM en Cloudflare DNS) para poder mandar emails como
notificaciones@nuevo-dominio. - Email Routing en Cloudflare: repetir setup
soporte@ydisputas@para el nuevo dominio. - Sentry: agregar el nuevo dominio a CORS allowlist en cada proyecto.
- Cloudflare Access (docs): actualizar la aplicación de Zero Trust si el dominio de docs también cambia.
Es un cambio grande — hacer en ventana de mantenimiento, con OK explícito.