Integración Stripe y Firebase
La monetización para un juego de navegador es un problema técnico pequeño-pero-complicado. Aquí está cómo CONTRABAND maneja las compras con Stripe, verificación con Firebase y sincronización cross-device vía Firestore.
Los requisitos
Los jugadores pueden comprar cuatro epílogos pagados ($4,99 cada uno), un bundle ($12,99) y cosméticos ocasionales ($2,99). Requisitos:
- Verificación server-side. Los flags "comprado" del lado cliente son fácilmente falsificables. El servidor debe saber autoritativamente quién ha comprado qué.
- Sincronización cross-device. Compra un epílogo en tu móvil, desbloquea en tu portátil en el próximo inicio de sesión.
- Tolerancia a offline. El juego debería correr sin conexión al servidor excepto en momentos de compra y momentos de sync-in.
- Compras de invitado. Los jugadores deberían poder comprar sin crear cuenta, luego vincular cuando se registren.
La arquitectura
Cuatro Vercel Functions manejan el flujo de pago completo:
- checkout.js — Crea una sesión de Stripe Checkout con el catálogo de productos. Devuelve URL de sesión al cliente, que redirige el navegador.
- stripe-webhook.js — Escucha eventos de Stripe (checkout.session.completed). Tras pago exitoso, escribe entitlements a Firestore bajo el UID del usuario (o token de invitado si no está autenticado).
- verify-purchase.js — Llamado por el cliente al cargar el juego. Devuelve los entitlements actuales del usuario desde Firestore. El cliente cachea el resultado a localStorage.
- link-purchase.js — Llamado cuando un comprador invitado luego crea una cuenta. Vincula las compras por token de invitado al nuevo UID.
Código total entre estas cuatro funciones: aproximadamente 280 líneas de Node.js. Comparten un módulo diminuto de inicialización Stripe/Firebase admin (~20 líneas).
El modelo de datos
Firestore tiene una sola colección: users. Cada documento clasificado por UID de Firebase contiene:
entitlements— Array de IDs de producto que el usuario ha comprado.guestTokens— Array de tokens de invitado vinculados a esta cuenta (usado para atribución de compra antes del registro).purchaseHistory— Array de {productId, timestamp, stripeSessionId} para auditoría.
Las compras de invitado van a una colección separada guestEntitlements, clasificada por un token generado por cliente en la primera visita. Cuando un invitado se registra, link-purchase.js mueve sus entitlements al documento de usuario autenticado.
El flujo del cliente
Flujo de compra del cliente:
- El usuario hace clic en "Comprar Epílogo" en la UI del juego.
- El cliente llama a
checkout.jscon productId y estado de auth actual (UID si está conectado, token de invitado sino). - checkout.js crea la sesión de Stripe, devuelve URL.
- El cliente redirige a la página de pago alojada por Stripe.
- El jugador ingresa detalles de pago, envía.
- Stripe redirige de vuelta al juego con éxito/cancelación.
- El webhook de Stripe se dispara server-side, escribe entitlement a Firestore.
- El cliente re-ejecuta verify-purchase.js al regresar, obtiene entitlements actualizados, cachea a localStorage.
- El contenido del epílogo se desbloquea en el juego.
El viaje de ida y vuelta es aproximadamente 8-12 segundos desde clic en botón hasta desbloqueo, la mayor parte del cual es la UI propia de Stripe. El trabajo server-side es de sub-500ms.
Por qué localStorage como caché
El juego no siempre puede alcanzar Firestore. Un jugador en una mala conexión, un avión o un entorno de navegador sandboxed puede fallar al sincronizar entitlements. Cachear entitlements confirmados por servidor a localStorage significa que el juego corre correctamente offline mientras el jugador haya sincronizado exitosamente alguna vez.
El modelo de seguridad es: localStorage es confiado por el cliente para lectura (mostrar UI), pero cualquier escritura debe ser confirmada por servidor. Un usuario malicioso que modifica localStorage obtiene una UI local falsamente desbloqueada, pero no se renderiza contenido real porque el contenido real del epílogo se carga desde un endpoint de servidor que verifica Firestore antes de servirlo. El caché localStorage es una optimización de UX, no una autoridad.
Trampas comunes
- Condiciones de carrera del webhook. Si un usuario regresa de Stripe antes de que se dispare el webhook, verify-purchase.js devuelve datos obsoletos. Arreglo: el cliente consulta verify-purchase cada 2 segundos durante 30 segundos tras el regreso de Stripe hasta que el nuevo entitlement aparezca.
- Pérdida de token de invitado. Si un jugador compra como invitado y luego limpia localStorage, su token se pierde y la compra queda huérfana. Arreglo: el recibo por email de Stripe contiene un "enlace de recuperación" que verifica contra el ID de sesión de Stripe y re-vincula la compra a un nuevo token.
- Secretos de firma de webhook. Los webhooks de Stripe deben verificar firmas. No hacerlo permite a cualquiera POSTear compras falsas. Este es el error #1 en integraciones de Stripe y lo cometí una vez.
Lo que aprendí
Stripe + Firebase + Vercel es genuinamente el camino más corto a un sistema de monetización funcional para un juego de navegador. El coste total de infraestructura es <$10/mes a cualquier escala razonable. El código es lo suficientemente corto para que un solo desarrollador lo mantenga.
El insight arquitectónico clave es el patrón caché-con-fuente-autoritativa. Firestore es la fuente de verdad; localStorage es caché; el cliente se confía a sí mismo para UI pero al servidor para acceso real de contenido. Este patrón escala de un jugador pagador a decenas de miles sin cambiar fundamentalmente.