Las apps SaaS tempranas adoran un flag local IsActive. Se siente rápido. También es la forma clásica de pasar un viernes reconciliando a un cliente que pagó, una fila que sigue en plan gratis y un webhook que ya procesaste dos veces.
El estado es una máquina, no un checkbox
Las suscripciones de Stripe atraviesan estados reales: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid y a veces paused. Cada uno significa algo distinto para el acceso.
incomplete no es "casi activo". Significa que la primera factura no se cobró. past_due no es cancelado. Los reintentos todavía pueden funcionar. paused después de un trial sin método de pago no es lo mismo que pausar la cobranza mientras las facturas siguen generándose.
Si tu app guarda un booleano y lo actualiza "cuando parece correcto", inventas una segunda fuente de verdad. Esa segunda fuente se desvía.
Refleja al proveedor, no inventes la tuya
El patrón pragmático es aburrido y fiable:
- Persiste el id de suscripción del proveedor y el string de estado en bruto.
- Deriva el acceso al producto de ese estado (y de tus reglas de entitlement), no de un flag mantenido a mano.
- Trata el webhook como el camino de escritura que refresca el estado. Trata tu admin como lector, no como un libro mayor rival.
En .NET eso suele significar mapear el enum de estado de Stripe a tu dominio una sola vez, y negarte a inventar enums paralelos que casi coinciden. El "casi" es donde viven los bugs.
Webhooks sin idempotencia te cobran dos veces
Los reintentos ocurren. La red falla. Tu handler verá el mismo event.id más de una vez. Sin una restricción única sobre ese id (o una tabla de claim equivalente), aplicas efectos secundarios dos veces: desbloqueas dos veces, envías el email dos veces o, peor, mutas el billing dos veces.
Verifica firmas contra el body en bruto. Usa claves de idempotencia en las llamadas API que no sean GET. Mantén el handler delgado: actualiza la proyección local, encola trabajo si hace falta, responde 200.
Stripe doméstico, Paddle global, un solo modelo de acceso
Ya sea que el cobro haya llegado por Stripe en casa o por un Merchant of Record en el exterior, tu producto necesita una sola respuesta a "¿puede esta cuenta usar la función hoy?" Deja los detalles del proveedor en el borde. Deja la lógica de entitlement en un solo lugar que lea un estado normalizado.
Una base code-first que ya cablea webhooks de billing, proyección de estado y una sola comprobación de entitlement te ahorra la tarde que pasarías depurando por qué producción dice pagado y la base de datos dice gratis. Esa tarde siempre cuesta más de lo que parecía la plantilla en la página de precios.