Ir al contenido

Esta guía está escrita para alguien que nunca ha desplegado esta aplicación antes. Sigue los pasos en orden.

📢 Nuevo: El proyecto ahora incluye CI/CD con GitHub Actions. Ver guía de CI/CD para:

  • Build automático en cada PR
  • Type checking automático
  • Despliegue automático a staging (próximamente)

Esta guía cubre el despliegue manual inicial a producción. Una vez configurado, el CI/CD puede automatizar deployments futuros.

Esta guía cubre Fly.io, pero también puedes desplegar en:

  • Railway.app - Interfaz web más simple, $5/mes fijo
  • Vercel/Netlify - Solo frontend (requiere backend separado)
  • AWS ECS/Fargate - Más caro pero más control

Recomendación: Fly.io para minimizar costos ($0 en free tier), Railway para simplicidad ($5/mes).

Usuarios
Fly.io (frontend + backend Wasp)
├── Neon.tech (PostgreSQL — más barato que Fly Postgres)
└── AWS S3 + CloudFront ─┐
├─ (videos de cursos — elige uno)
└── Azure Blob Storage ─┘

¿Por qué esta combinación?

ServicioUsoCosto estimado
Fly.ioApp Wasp (servidor + cliente)~$5-10/mes
Neon.techBase de datos PostgreSQLGratis (hasta 0.5 GB) / $19/mes (10 GB)
AWS S3 + CloudFrontAlmacenamiento de videos (opción A)~$0.023/GB/mes + CDN
Azure Blob StorageAlmacenamiento de videos (opción B)~$0.018/GB/mes + egress

Fase 1 — Preparación (Antes de Desplegar)

Sección titulada «Fase 1 — Preparación (Antes de Desplegar)»

Antes de empezar, debes tener:

  • Cuenta en Fly.io (tarjeta de crédito requerida para apps de producción)
  • Cuenta en Neon.tech (plan gratuito disponible)
  • Almacenamiento de videos — elige una de estas opciones:
    • Cuenta en AWS con acceso a S3 y CloudFront, o
    • Cuenta en Azure con una Storage Account
  • Cuenta en Stripe con verificación completa
  • Cuenta en SendGrid para emails de producción
  • Wasp CLI instalado: curl -sSL https://get.wasp-lang.dev/installer.sh | sh
Ventana de terminal
# macOS
brew install flyctl
# Linux
curl -L https://fly.io/install.sh | sh
# Verificar instalación
flyctl version
# Iniciar sesión
flyctl auth login

Neon es una alternativa más barata que Fly Postgres. El plan gratuito cubre el inicio.

  1. Ir a neon.tech y crear una cuenta
  2. Hacer clic en “Create Project”
  3. Nombre del proyecto: talentbricksai
  4. Región: US East (N. Virginia) — cercana a la región de Fly que usarás
  5. PostgreSQL version: 16 (o la más reciente disponible)
  1. En el dashboard de Neon, ir a Connection Details
  2. Copiar la Connection String — se ve así:
postgresql://username:password@ep-xxx-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
  1. Guardar esta URL — la necesitarás en Fase 4.

Importante: Neon requiere ?sslmode=require al final de la URL. Wasp lo necesita también.


La app soporta dos proveedores de almacenamiento. La variable STORAGE_PROVIDER controla cuál se usa en runtime (aws por defecto si no se configura).

STORAGE_PROVIDER=aws # usa AWS S3 + CloudFront
STORAGE_PROVIDER=azure # usa Azure Blob Storage

Elige una de las siguientes opciones:


  1. Ir a AWS Console → S3
  2. Hacer clic en “Create bucket”
  3. Configuración:
    • Bucket name: talentbricksai-videos (debe ser único globalmente)
    • AWS Region: us-east-1 (North Virginia) — buena cobertura global
    • Block all public access: ✅ Activado (los videos serán privados, accedidos via CloudFront)
  4. Hacer clic en “Create bucket”
  1. Ir a AWS Console → IAM
  2. Users → Create user
  3. Nombre: talentbricksai-app
  4. En Permissions, elegir “Attach policies directly”
  5. Buscar y seleccionar AmazonS3FullAccess (o crear una política más restrictiva — ver abajo)
  6. Crear el usuario y luego ir a Security credentials → Create access key
  7. Propósito: Application running outside AWS
  8. Guardar Access Key ID y Secret Access Key — no se pueden ver de nuevo

Política IAM mínima (más segura):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::talentbricksai-videos", "arn:aws:s3:::talentbricksai-videos/*"]
}
]
}

CloudFront es el CDN que entrega los videos rápidamente a cualquier parte del mundo.

  1. Ir a AWS Console → CloudFront
  2. Hacer clic en “Create distribution”
  3. Origin domain: seleccionar el bucket S3 que creaste
  4. Origin access: elegir “Origin access control settings (recommended)”
    • Crear un nuevo OAC con el nombre del bucket
  5. Viewer protocol policy: Redirect HTTP to HTTPS
  6. Cache policy: CachingOptimized (default — buena para videos)
  7. Hacer clic en “Create distribution”

Después de crear:

  • Copiar el Distribution domain name (ej. d1234abc.cloudfront.net)
  • CloudFront tardará ~10 minutos en desplegarse globalmente

Autorizar CloudFront a acceder al bucket S3:

Después de crear la distribución, CloudFront te pedirá actualizar la política del bucket. Copiar y pegar la política que muestra en el bucket S3 → Permissions → Bucket policy.

3.A.4 Configurar Signed URLs (para videos privados)

Sección titulada «3.A.4 Configurar Signed URLs (para videos privados)»

Los videos de cursos deben ser privados — solo accesibles por usuarios con acceso pago. Para esto se usan CloudFront Signed URLs.

  1. En CloudFront → tu distribución → Key management
  2. Key groups → Create key group
  3. Primero crear un Key pair (CloudFront → Key management → Public keys):
    Ventana de terminal
    # Generar key pair localmente
    openssl genrsa -out cloudfront_private_key.pem 2048
    openssl rsa -pubout -in cloudfront_private_key.pem -out cloudfront_public_key.pem
  4. Subir cloudfront_public_key.pem a CloudFront → Public keys
  5. Copiar el Key ID que asigna CloudFront
  6. Guardar el contenido de cloudfront_private_key.pem — necesario como variable de entorno

Azure Blob Storage es más sencillo de configurar y no requiere un CDN separado. Las URLs de descarga son SAS URLs temporales (firmadas), igual que CloudFront Signed URLs.

  1. Ir a Azure Portal → Storage accounts
  2. Hacer clic en “Create”
  3. Configuración:
    • Storage account name: talentbricksai (solo minúsculas y números, único globalmente)
    • Region: elige la más cercana a tus usuarios (ej. East US)
    • Performance: Standard
    • Redundancy: LRS (Locally Redundant Storage — suficiente para empezar)
  4. Hacer clic en “Review + Create”“Create”
  1. Dentro de la Storage Account → Containers → + Container
  2. Nombre: videos
  3. Public access level: Private (sin acceso anónimo — la app genera SAS URLs)
  4. Hacer clic en “Create”
  1. Dentro de la Storage Account → Security + networking → Access keys
  2. Copiar el Storage account name y una de las Keys (key1 o key2)
  3. Guardar ambos valores — son las credenciales de la app

Desde el directorio app/:

Ventana de terminal
cd app
wasp deploy fly launch talentbricksai mia
  • talentbricksai: nombre de la app (debe ser único en Fly.io)
  • mia: región Miami — cercana a Latinoamérica

Este comando creará dos apps en Fly.io: talentbricksai (servidor) y talentbricksai-client (frontend). No creará la base de datos — usaremos Neon en su lugar.

4.2 Configurar Variables de Entorno del Servidor

Sección titulada «4.2 Configurar Variables de Entorno del Servidor»

Ejecutar cada comando desde el directorio app/:

Ventana de terminal
# === BASE DE DATOS (Neon) ===
wasp deploy fly cmd --context server secrets set \
DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"
# === JWT (generar uno seguro) ===
# Para generar: openssl rand -base64 32
wasp deploy fly cmd --context server secrets set \
JWT_SECRET="tu-jwt-secret-generado-con-openssl"
# === STRIPE (modo producción — sk_live_*) ===
wasp deploy fly cmd --context server secrets set \
STRIPE_API_KEY="sk_live_..."
wasp deploy fly cmd --context server secrets set \
STRIPE_WEBHOOK_SECRET="whsec_..."
# === PLANES DE STRIPE (IDs de producción) ===
wasp deploy fly cmd --context server secrets set \
PAYMENTS_MONTHLY_SUBSCRIPTION_PLAN_ID="price_live_..."
wasp deploy fly cmd --context server secrets set \
PAYMENTS_ANNUAL_SUBSCRIPTION_PLAN_ID="price_live_..."
wasp deploy fly cmd --context server secrets set \
PAYMENTS_CREDITS_10_PLAN_ID="price_live_..."
# === ALMACENAMIENTO DE VIDEOS — elige UNA opción ===
# --- Opción A: AWS S3 + CloudFront ---
wasp deploy fly cmd --context server secrets set \
STORAGE_PROVIDER="aws"
wasp deploy fly cmd --context server secrets set \
AWS_S3_IAM_ACCESS_KEY="AKIA..."
wasp deploy fly cmd --context server secrets set \
AWS_S3_IAM_SECRET_KEY="tu-secret-key"
wasp deploy fly cmd --context server secrets set \
AWS_S3_FILES_BUCKET="talentbricksai-videos"
wasp deploy fly cmd --context server secrets set \
AWS_S3_REGION="us-east-1"
# CloudFront (para videos privados con Signed URLs):
wasp deploy fly cmd --context server secrets set \
CLOUDFRONT_DOMAIN="d1234abc.cloudfront.net"
wasp deploy fly cmd --context server secrets set \
CLOUDFRONT_KEY_PAIR_ID="K1234..."
# El private key va en una línea (reemplazar saltos de línea con \n)
wasp deploy fly cmd --context server secrets set \
CLOUDFRONT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----"
# --- Opción B: Azure Blob Storage ---
wasp deploy fly cmd --context server secrets set \
STORAGE_PROVIDER="azure"
wasp deploy fly cmd --context server secrets set \
AZURE_STORAGE_ACCOUNT_NAME="talentbricksai"
wasp deploy fly cmd --context server secrets set \
AZURE_STORAGE_ACCOUNT_KEY="tu-account-key"
wasp deploy fly cmd --context server secrets set \
AZURE_STORAGE_CONTAINER_NAME="videos"
# === SENDGRID (emails de producción) ===
wasp deploy fly cmd --context server secrets set \
SENDGRID_API_KEY="SG...."
# === GOOGLE OAUTH (si está habilitado) ===
wasp deploy fly cmd --context server secrets set \
GOOGLE_CLIENT_ID="722...apps.googleusercontent.com"
wasp deploy fly cmd --context server secrets set \
GOOGLE_CLIENT_SECRET="GOC..."
# === ADMIN EMAILS ===
wasp deploy fly cmd --context server secrets set \
ADMIN_EMAILS="tu@email.com"
# === NOTIFICACIONES B2B (opcional) ===
wasp deploy fly cmd --context server secrets set \
SALES_NOTIFICATION_EMAIL="ventas@tuempresa.com"
# === URL del cliente ===
wasp deploy fly cmd --context server secrets set \
CLIENT_URL="https://talentbricksai-client.fly.dev"
# === OPENAI (opcional — para features de AI) ===
wasp deploy fly cmd --context server secrets set \
OPENAI_API_KEY="sk-..."
# === ANALYTICS (opcional) ===
wasp deploy fly cmd --context server secrets set \
PLAUSIBLE_API_KEY="..."
wasp deploy fly cmd --context server secrets set \
PLAUSIBLE_SITE_ID="talentbricks.ai"
Ventana de terminal
wasp deploy fly cmd --context client secrets set \
REACT_APP_GOOGLE_ANALYTICS_ID="G-XXXXXXXXXX"
Ventana de terminal
cd app
wasp deploy fly deploy

Este comando:

  1. Construye el servidor Node.js
  2. Construye el cliente React
  3. Sube ambos a Fly.io
  4. Ejecuta las migraciones de base de datos automáticamente
Ventana de terminal
# Ver logs en tiempo real
flyctl logs -a talentbricksai
# Ver estado de la app
flyctl status -a talentbricksai
# Ver si las máquinas están corriendo
flyctl machines list -a talentbricksai

La app debería estar disponible en:

  • Servidor: https://talentbricksai.fly.dev
  • Cliente: https://talentbricksai-client.fly.dev

Fase 5 — Configurar Stripe para Producción

Sección titulada «Fase 5 — Configurar Stripe para Producción»

5.1 Completar Verificación de Cuenta Stripe

Sección titulada «5.1 Completar Verificación de Cuenta Stripe»
  1. Ir a Stripe Dashboard
  2. Completar la verificación de negocio (puede tomar 1-2 días)
  3. Cambiar a Live mode (toggle en la esquina superior izquierda)

En Live mode:

  1. Developers → API keys
  2. Copiar Secret key (comienza con sk_live_)
  3. Copiar Publishable key (comienza con pk_live_)

5.3 Crear Productos y Precios en Producción

Sección titulada «5.3 Crear Productos y Precios en Producción»

Repetir la configuración de productos de test, pero en Live mode:

  1. Products → Add product
  2. Crear: Suscripción Mensual ($19), Suscripción Anual ($149), cursos individuales ($29-99)
  3. Copiar los Price IDs (comienzan con price_) y actualizar las variables de entorno
  1. Developers → Webhooks → Add endpoint
  2. Endpoint URL: https://talentbricksai.fly.dev/payments-webhook
  3. Eventos a escuchar:
    • checkout.session.completed
    • invoice.paid
    • customer.subscription.updated
    • customer.subscription.deleted
  4. Copiar el Signing secret y actualizar STRIPE_WEBHOOK_SECRET

Ventana de terminal
flyctl ips allocate-v4 -a talentbricksai-client
flyctl ips list -a talentbricksai-client

En tu proveedor de DNS (Namecheap, Cloudflare, etc.):

TipoHostValor
A@IP del cliente de Fly.io
AwwwIP del cliente de Fly.io
CNAMEapitalentbricksai.fly.dev
Ventana de terminal
flyctl certs add talentbricks.ai -a talentbricksai-client
flyctl certs add www.talentbricks.ai -a talentbricksai-client
Ventana de terminal
wasp deploy fly cmd --context server secrets set CLIENT_URL="https://talentbricks.ai"

Luego redesplegar:

Ventana de terminal
cd app && wasp deploy fly deploy

NUNCA ejecutar wasp deploy fly deploy directamente. Siempre usar:

Ventana de terminal
cd app
make deploy

make deploy hace dos cosas en orden:

  1. make deploy-client — construye con Wasp y despliega el cliente usando un Dockerfile basado en nginx (en lugar del gostatic que genera Wasp). Esto es lo que aplica los security headers. El REACT_APP_API_URL se pasa automáticamente apuntando a talentbricksai.fly.dev.

  2. make deploy-server — despliega el servidor con --skip-client para no sobreescribir el Dockerfile de nginx.

También puedes desplegar cada componente por separado:

Ventana de terminal
make deploy-client # solo frontend (nginx + security headers)
make deploy-server # solo backend

¿Por qué no usar wasp deploy fly deploy directamente?

  • Sin --skip-client: sobreescribe el Dockerfile del cliente con gostatic, que no soporta headers HTTP, eliminando todos los security headers.
  • Sin REACT_APP_API_URL: Wasp genera la URL del servidor como talentbricksai-server.fly.dev, que no existe. Nuestro servidor se llama talentbricksai (sin -server).

Los archivos clave del cliente:

  • app/client-Dockerfile + app/client-nginx.conf — fuente de verdad para nginx
  • app/scripts/deploy-client.sh — script invocado por make deploy-client

Security headers aplicados por nginx:

  • Content-Security-Policy (CSP)
  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy
  • Strict-Transport-Security (HSTS)
  1. Commits de código: Commitear y pushear cambios a git primero
  2. Variables de entorno: Actualizar secrets de Fly si es necesario
  3. Deploy: cd app && make deploy
  4. Verificación: Revisar logs y probar funcionalidad crítica
Ventana de terminal
# Ver logs en tiempo real durante el deploy
flyctl logs -a talentbricksai
# Verificar estado después del deploy
flyctl status -a talentbricksai
flyctl status -a talentbricksai-client
# Verificar security headers del cliente
curl -I https://talentbricksai-client.fly.dev/
# Debe mostrar: X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, etc.
# Verificar que el JS del cliente tiene la URL correcta
JS_FILE=$(curl -s https://talentbricksai-client.fly.dev/ | grep -o 'assets/[^"]*\.js' | head -1)
curl -s "https://talentbricksai-client.fly.dev/$JS_FILE" | grep -o 'https://talentbricksai[^"]*\.fly\.dev'
# Debe mostrar: https://talentbricksai.fly.dev

Ventana de terminal
# Todos los logs
flyctl logs -a talentbricksai
# Solo errores
flyctl logs -a talentbricksai | grep -i error
# Logs del cliente
flyctl logs -a talentbricksai-client

Visitar fly.io/dashboard:

  • Métricas de CPU/memoria
  • Estado de las máquinas
  • Historial de deployments

Visitar console.neon.tech:

  • Uso de almacenamiento
  • Queries activas
  • Métricas de rendimiento

Neon incluye backups automáticos en todos los planes. Para restaurar, usar el Point-in-time restore desde el dashboard.

Ventana de terminal
# Instalar pg_dump si no lo tienes
# macOS: brew install postgresql
# Hacer backup
pg_dump "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require" \
> backup-$(date +%Y%m%d).sql
# Restaurar
psql "postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require" \
< backup-20260217.sql

ServicioPlanCosto/mes
Fly.io — Servidorshared-cpu-1x, 512MB~$6.38
Fly.io — Clienteshared-cpu-1x, 256MB~$3.19
Neon — DBFree tier (0.5 GB)$0
AWS S3~50 GB almacenamiento~$1.15
AWS CloudFront~100 GB transferencia~$0.85
Total estimado~$12-15/mes
ServicioPlanCosto/mes
Fly.io — Servidorshared-cpu-2x, 512MB~$6.38
Fly.io — Clienteshared-cpu-1x, 256MB~$3.19
Neon — DBLaunch plan (10 GB)$19
AWS S3~500 GB~$11.50
AWS CloudFront~1 TB~$8.50
Total estimado~$50-60/mes

Verificar que la variable fue configurada correctamente:

Ventana de terminal
flyctl ssh console -a talentbricksai -C "env | grep DATABASE"

Error: “Connection refused” (base de datos)

Sección titulada «Error: “Connection refused” (base de datos)»
  1. Verificar que la URL de Neon incluye ?sslmode=require
  2. Verificar que Neon permite conexiones desde Fly.io (Neon permite todas por defecto)
  1. Verificar que STRIPE_WEBHOOK_SECRET corresponde al webhook de producción (no el de test)
  2. Verificar que la URL del webhook es https://talentbricksai.fly.dev/payments-webhook
  3. Ver logs: flyctl logs -a talentbricksai | grep -i stripe
  1. Verificar AWS_S3_IAM_ACCESS_KEY y AWS_S3_IAM_SECRET_KEY
  2. Verificar que el usuario IAM tiene los permisos correctos en S3
  3. Verificar que AWS_S3_FILES_BUCKET coincide exactamente con el nombre del bucket

Error: “Azure storage error” / “AuthenticationFailed”

Sección titulada «Error: “Azure storage error” / “AuthenticationFailed”»
  1. Verificar que AZURE_STORAGE_ACCOUNT_NAME es el nombre exacto de la Storage Account (sin espacios, minúsculas)
  2. Verificar que AZURE_STORAGE_ACCOUNT_KEY es la key completa (base64, ~88 caracteres)
  3. Verificar que AZURE_STORAGE_CONTAINER_NAME coincide con el container creado en Azure
  4. Verificar que STORAGE_PROVIDER=azure está configurado
Ventana de terminal
# Ver estado
flyctl status -a talentbricksai
# Reiniciar si está en estado incorrecto
flyctl apps restart talentbricksai
# Ver máquinas
flyctl machines list -a talentbricksai

Error: CORS / App crashea constantemente / Health checks fallan

Sección titulada «Error: CORS / App crashea constantemente / Health checks fallan»

Síntomas:

  • Errores CORS: “No ‘Access-Control-Allow-Origin’ header”
  • Health checks fallando repetidamente
  • App se reinicia constantemente
  • Logs muestran: Out of memory: Killed process

Causa: Memoria insuficiente (256 MB es muy poco para Node.js + Prisma + Express).

Solución:

  1. Actualizar fly-server.toml (configuración permanente):
[[vm]]
size = "shared-cpu-1x"
memory = "512mb" # Cambiado de 256mb
  1. Aplicar el cambio inmediatamente (sin redesplegar):
Ventana de terminal
flyctl scale memory 512 -a talentbricksai
  1. Verificar que funciona:
Ventana de terminal
flyctl status -a talentbricksai
# Debe mostrar: Memory: 512 MB

Nota importante: Con 256 MB, el servidor crashea antes de poder enviar respuestas (incluidos headers CORS), causando errores engañosos de CORS cuando en realidad es un problema de memoria.

Error: “ERR_NAME_NOT_RESOLVED” al cargar el cliente

Sección titulada «Error: “ERR_NAME_NOT_RESOLVED” al cargar el cliente»

Síntoma: El cliente muestra errores de conexión al servidor:

POST https://talentbricksai-server.fly.dev/operations/... net::ERR_NAME_NOT_RESOLVED

Causa: Se ejecutó wasp deploy fly deploy sin --skip-client, lo que reemplaza el Dockerfile del cliente con la versión de gostatic que no tiene la URL correcta del servidor.

Solución: Redesplegar el cliente usando el script correcto:

Ventana de terminal
cd app
./scripts/deploy-client.sh

El script construye con Wasp y luego sobreescribe el Dockerfile con la versión nginx que incluye los security headers y la configuración correcta.


Base de datos:

  • Neon proyecto creado y connection string guardada
  • DATABASE_URL configurada en Fly con ?sslmode=require
  • Migraciones ejecutadas (el primer deploy las corre automáticamente)

Pagos:

  • Stripe en modo Live (no Test)
  • Productos y precios creados en Live mode
  • STRIPE_API_KEY es sk_live_* (no sk_test_*)
  • Webhook configurado con URL de producción
  • STRIPE_WEBHOOK_SECRET del webhook de producción

Videos (AWS S3 + CloudFront):

  • STORAGE_PROVIDER=aws configurado en Fly
  • Bucket S3 creado con acceso público bloqueado
  • CloudFront distribución creada y apuntando al bucket
  • Policy del bucket actualizada para CloudFront OAC
  • Key pair de CloudFront generado y configurado para Signed URLs
  • Variables AWS_S3_* y CLOUDFRONT_* configuradas en Fly

Videos (Azure Blob Storage — alternativa a AWS):

  • STORAGE_PROVIDER=azure configurado en Fly
  • Storage Account creada en Azure
  • Container videos creado con acceso privado
  • Variables AZURE_STORAGE_ACCOUNT_NAME, AZURE_STORAGE_ACCOUNT_KEY, AZURE_STORAGE_CONTAINER_NAME configuradas en Fly

Email:

  • SendGrid cuenta verificada y dominio autenticado
  • SENDGRID_API_KEY configurada
  • Email de “From” verificado en SendGrid
  • SALES_NOTIFICATION_EMAIL configurada (opcional — para recibir alertas de demos B2B)

App:

  • CLIENT_URL apunta al dominio correcto (con https://)
  • ADMIN_EMAILS incluye los emails de los admins
  • JWT_SECRET es aleatorio y seguro (mínimo 32 caracteres)

Verificación:

  • Login funciona con un usuario real
  • Email de verificación llega correctamente
  • Pago de prueba con tarjeta real de Stripe completa
  • Video de curso carga desde CloudFront
  • Panel admin accesible con cuenta admin