Asterias
Empezar

El programa de referidos de Asterias, en código abierto

Asterias es un producto cerrado, pero una de sus piezas ya no lo es. El programa de referidos que funciona aquí en producción se ha extraído, reescrito y publicado con licencia MIT. Esto es lo que hace, cómo lo hace, y las tres trampas que existe para evitar.

Licencia
MIT
Construido sobre
Next.js 15 · Prisma 6 · Stripe 18 · TypeScript
Pruebas unitarias
89
Idiomas
EN · FR · DE · IT · ES

Qué hace este módulo

Casi todos los programas de referidos premian un registro. Un registro no cuesta nada de fabricar, así que esos programas acaban explotados o envueltos en reglas antifraude que nadie sabe explicar a un cliente de buena fe.

Este premia ingresos, y solo mientras duran. Cada referido que se convierte en cliente de pago rebaja una parte de la suscripción de quien lo invitó. El día que deja de pagar, esa parte vuelve en la factura siguiente.

  • Sarah invita a Tom. Tom se registra: Sarah no gana nada.
  • Tom empieza una prueba gratuita: Sarah sigue sin ganar nada.
  • Se cobra la tarjeta de Tom: Sarah obtiene un 20 % de descuento.
  • El pago de Tom falla: Sarah vuelve al precio completo.
  • Tom paga de nuevo: el descuento regresa.

Cinco referidos que pagan y la suscripción de Sarah es gratuita. El paso y el tope se fijan en un archivo de entorno.

No hay hucha de puntos, ni libro de créditos, ni cola de pagos. El descuento es una función de cuántos referidos están pagando en este instante, recalculada desde la base de datos y escrita en Stripe. Eso es lo que impide que la pantalla y la factura digan dos cosas distintas, porque ambas leen la misma función.

Tres trampas, y qué hace este módulo con ellas

Tres cosas salen mal en casi todos los programas de referidos escritos a mano. Este módulo es sobre todo la forma que toma evitarlas.

La trampaQué ocurreQué hace este módulo
Un cupón de un solo usoParece la manera limpia de hacer cierto el «recalculado cada mes»: se adjunta un cupón, se cae, se adjunta el siguiente. El descuento depende entonces de que algo lo vuelva a adjuntar antes de cada factura, y cualquier ventana en la que eso no se ejecutó es un cobro a precio completo que nadie señala.duration: forever. El descuento es un estado duradero que el módulo retira cuando lo decide. Una sincronización tardía no cuesta nada.
Creerse el contenido del webhookStripe no garantiza el orden de entrega. Un evento past_due puede llegar después del active que lo había sustituido. Escribir lo que dice el payload es mover el descuento de la suscripción de un tercero, basándose en un estado que ya no es cierto.Cada evento desencadena una lectura nueva de la API de Stripe. Dos tratamientos concurrentes se ordenan por la marca de tiempo de su lectura.
Creerse el webhook, sin másUn despliegue, un error 500, un evento que Stripe ha dejado de reintentar: quien invitó se queda en el nivel equivocado, y un descuento equivocado se parece exactamente a uno correcto.Un reconciliador recalcula todos los niveles desde la base de datos, repara la desviación y dice qué ha reparado.

Cómo se calcula el descuento

No hay, deliberadamente, ningún «descuento actual» almacenado. El nivel es una función pura de cuántos referidos están pagando en este instante.

discountPercent(activeReferrals) =
  min(max(activeReferrals, 0), MAX_REFERRALS) × PERCENT_STEP

La misma función alimenta la página del cliente y la sincronización con Stripe, así que lo que se le muestra y lo que se cobra a su tarjeta no pueden discrepar porque dos sitios se hayan desincronizado.

El cliente lo nota en su factura siguiente: los descuentos se escriben con proration_behavior en none, de modo que un cambio de nivel nunca genera un abono ni un cobro inmediato por un mes ya servido.

Estado en Stripe¿Cuenta?Por qué
activeEl referido paga.
trialingSegún el ajusteSolo si REFERRAL_COUNT_TRIALING vale true. Lea la sección sobre el abuso antes de tocarlo.
past_dueNoUna renovación ha fallado. Eso es lo que significa «el descuento se detiene al primer impago».
unpaidNoLa factura nunca se saldó.
canceledNoLa suscripción está cancelada.
pausedNoLa suscripción está en pausa, así que no se cobra nada.
incompleteNoNingún pago llegó a completarse. Lo mismo para una cuenta sin suscripción o en un plan gratuito.

Configuración

Todo vive en el entorno y se valida al arrancar. Una configuración que no puede cumplirse se niega a arrancar, en lugar de fallar el día en que un cliente alcanza el nivel más alto.

VariablePor defectoQué hace
REFERRAL_PERCENT_STEP20Porcentaje de descuento por cada referido que paga.
REFERRAL_MAX_REFERRALS5Cuántos referidos siguen dando descuento.
REFERRAL_COUNT_TRIALINGfalse¿Cuenta un referido que todavía está en su prueba gratuita? Lea la sección sobre el abuso antes de ponerlo en true.
REFERRAL_COUPON_PREFIXreferral_offPrefijo de los cupones que crea esta aplicación. El porcentaje se añade al final.
REFERRAL_COOKIE_DAYS30Cuánto sobrevive un código capturado antes del registro.
APP_URLningunoOrigen público. Los enlaces de referido se construyen a partir de él.
STRIPE_SECRET_KEYningunoSin ella los referidos se registran y se muestran, pero no se aplica ningún descuento.
STRIPE_WEBHOOK_SECRETningunoSin él, el endpoint lo rechaza todo. Ambos hacen falta para la mitad de facturación.

Elegir el paso y el tope

REFERRAL_PERCENT_STEP multiplicado por REFERRAL_MAX_REFERRALS debe quedarse en 100 o por debajo. Stripe no tiene cupones de más del 100 % de descuento, y un nivel que pidiera uno caería en silencio en ningún descuento, justo para la cuenta que más lo había ganado.

20 × 5   # a referral is worth 20%, five make it free      (default)
10 × 5   # a gentler program that caps at half price
25 × 2   # two referrals, half price, nothing beyond
33 × 3   # 99% at the top; the last 1% keeps a card on file

Con un 100 % de descuento, Stripe emite una factura a cero y no cobra la tarjeta. La tarjeta deja de ponerse a prueba, y cuando el nivel vuelve a ser de pago ese primer cobro real puede fallar en una tarjeta caducada hace meses. Por eso la página de referidos indica el importe de la próxima factura, y por eso 33 × 3 aparece en la lista: se detiene en el 99 % y mantiene un pago real en la cuenta.

Probarlo en un minuto

El repositorio es una aplicación Next.js completa: el módulo, una base de datos Postgres, cuentas de demostración y la página de referidos.

git clone https://github.com/MaxenceLassus/nextjs-stripe-referrals
cd nextjs-stripe-referrals
pnpm install

docker compose up -d          # Postgres on :5433
cp .env.example .env.local    # works as-is without Stripe
pnpm db:migrate && pnpm db:seed
pnpm dev

Sin clave de Stripe el programa registra los referidos, los muestra, no aplica ningún descuento y lo dice en pantalla. Una clave de prueba y un secreto de webhook encienden la mitad de facturación.

Para ver los dos lados en un solo navegador: regístrese, copie su enlace, ábralo en una ventana privada y regístrese como otra persona, suscriba esa segunda cuenta y mire la página de la primera pasar al 20 % con su próxima factura indicada. Cancele la segunda suscripción y vea desaparecer el descuento.

Instalarlo en su aplicación

Todo el módulo cabe en una carpeta, src/referrals. Se copia, y quedan tres cosas por hacer.

Los cuatro modelos de Prisma que añade a su esquema no llevan ninguna clave externa hacia su tabla de usuarios: userId es una cadena opaca, lo que su aplicación llame identificador de cuenta. Eso es lo que lo convierte en una carpeta que se copia y no en una migración que se fusiona. A cambio, nada se borra en cascada al eliminar una cuenta, de ahí forgetUser(userId), que debe llamar desde su propia ruta de borrado.

1. Atribuir la cuenta en el registro

attachReferral nunca lanza una excepción. Un referido es una cortesía comercial, un registro es el negocio: un código caducado, mal escrito, autorreferente o falsificado devuelve un resultado que puede registrar e ignorar, y la cuenta se crea en cualquier caso.

import { attachReferral, readReferralCookie, clearReferralCookie, accountLabel } from '@/referrals'

const user = await createYourAccount(...)

await attachReferral(user.id, await readReferralCookie(), {
  label: accountLabel({ name: user.name, email: user.email }),
})
await clearReferralCookie()

La gente comparte la página que la convenció, y esa es mucho más a menudo la página de precios que el formulario de registro. Una línea en el middleware que ya tiene captura ?ref= en todas las páginas.

2. Capturar el código en todas partes, no solo en /signup

El código capturado sobrevive treinta días por defecto, el tiempo que necesita un visitante para irse, pensárselo y volver a crear una cuenta.

// src/middleware.ts
export function middleware(request: NextRequest) {
  return captureReferral(request, NextResponse.next())
}

3. Montar el webhook y marcar su checkout

El webhook es donde el programa se entera de que un referido ha pagado. El checkout, por su parte, le dice a qué cuenta pertenece el cliente de Stripe que acaba de crear.

// src/app/api/referrals/webhook/route.ts
export { POST } from '@/referrals/stripe/webhook'

// wherever you create a Checkout session
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price, quantity: 1 }],
  ...referralCheckoutOptions(user.id),
})

Y la página

ReferralPage recibe un userId y nunca averigua por sí misma quién ha iniciado sesión, a propósito. La autenticación es suya: un componente que resuelve la sesión por su cuenta es un componente que puede montarse en una ruta donde nadie ha comprobado nada.

// src/app/dashboard/referrals/page.tsx
export default async function Page() {
  const user = await requireUser()      // your auth, not this module's

  return <ReferralPage
    userId={user.id}
    locale={user.locale}
    label={user.name}
    price={{ amount: 2900, currency: 'eur' }}
  />
}

El abuso, y por qué este diseño lo resiste

La línea más importante de este módulo es que una prueba gratuita no cuenta.

Si las pruebas contaran, cualquiera abriría cinco cuentas, empezaría cinco pruebas, se llevaría un 100 % de descuento en su propia suscripción y cancelaría las cinco antes de que se tocara una tarjeta. El programa pagaría por nada.

Excluidas las pruebas, ganar un descuento exige suscripciones realmente pagadas. Para fingir cinco referidos que pagan hay que pagar de verdad cinco suscripciones para dejar una gratis. Es la economía la que vigila, y por eso aquí no hay ninguna puntuación de fraude que ajustar ni que explicar en un ticket de soporte.

Lo que queda de su lado: limitar la frecuencia de su ruta de registro. Este módulo no ve su tráfico, y la creación de cuentas es su endpoint.

El ataquePor qué falla
Invitarse a uno mismoresolveReferrer rechaza cuando el dueño del código es la cuenta que se está creando.
Dos padrinos para una misma cuentareferredUserId es único. Gana el primero, para siempre.
Falsificar la cookie o el valor de ?ref=Los dos los controla el visitante y no se cree a ninguno: el código se busca en la base de datos en el momento del registro. Falsificarlo solo da un descuento a la cuenta que nombra, así que no hay nada que robar.
Reclamar referidos de forma retroactivaattachReferral rechaza una cuenta que ya ha sido cliente.
Repetir un webhook de StripeCada id de evento se guarda antes de tratarlo, y la clave primaria descarta la repetición.
Leer los referidos de otra cuentaCada consulta está atada al userId que usted pasa. Pase uno autenticado.

Explotación

Cuatro comandos, de los cuales dos cuentan de verdad.

pnpm referrals:doctor              # will this deployment actually apply discounts?
pnpm referrals:reconcile           # repair drift; run hourly
pnpm referrals:reconcile --dry-run # report it without writing
pnpm referrals:verify-flow         # walk the whole program against a real database

reconcile solo habla cuando ha hecho algo. Un barrido que escribe un registro en cada pasada tranquila enseña a todo el mundo a ignorarlo, y entonces también se pierde el día ruidoso. Si repara veinte descuentos al día, algo está roto más arriba y hay que saberlo.

El diagnóstico, desde el servidor

doctor responde a la pregunta que no tiene respuesta en un navegador: un secreto de webhook que falta, una clave que apunta a la cuenta de Stripe equivocada, un cupón con la duración equivocada. Desde fuera, cada uno de esos casos se parece exactamente a un programa en el que nadie ha invitado todavía a nadie.

Configuration
  ok    tiers: 20% x 5 = 100% maximum
  ok    trials do not count
Stripe
  ok    key works: account acct_1234 (Your Company)
  ok    mode: test
Coupons
  ok    referral_off_20: 20% off, forever
  FAIL  referral_off_40 has duration "once", not "forever".
        A `once` coupon falls off after one invoice and the discount silently stops.

Qué está probado, y qué no

89 pruebas unitarias, sin base de datos y sin red: la aritmética de los niveles y sus límites, la generación y el escapado de los códigos, la validación de la configuración, la correspondencia de estados, las protecciones de la atribución, la firma, la idempotencia y el enrutado del webhook, y la sincronización del descuento, incluida su vía de error.

Ocho de ellas son pruebas de cable: el SDK real de Stripe, parámetros reales serializados, contra un grabador local. Comprueban los bytes y no un mock: duration=forever en el cupón, discounts[0][coupon] al aplicar, discounts vacío al retirar, proration_behavior=none en ambos casos. Son las tres cosas que más probablemente estén mal en silencio en una integración con Stripe, y las más invisibles para una prueba con mocks.

Lo que nada de esto cubre: cómo responde Stripe de verdad, y si una factura real sale más baja. Eso pide una clave de prueba y cinco minutos, y el repositorio explica los pasos.

Cinco idiomas

Inglés, francés, alemán, italiano, español. El inglés es el idioma por defecto y el repliegue de todo lo que no se reconoce; fr-CA se resuelve como fr. Los diccionarios están tipados por idioma, así que añadir un idioma, o incluso una sola cadena, no compila hasta que los cinco la tienen. Una prueba comprueba además que ninguna traducción pierda un marcador.

Lo que mueve aquí

No es una abstracción de fin de semana. Mueve el programa de referidos de Asterias, el software de reseñas de Google en cuyo sitio está usted. Un comerciante que invita a un colega ve bajar su suscripción un 20 % en cuanto ese colega paga de verdad, y subir de nuevo si deja de pagar.

La versión publicada no es una copia del código de aquí: es una reescritura, y corrige dos defectos reales encontrados al extraerla. Si gestiona algo parecido, merecen diez minutos de su tiempo.

Licencia

MIT. Copie la carpeta, cámbiela, venda lo que haga con ella. Si la pone en producción, un mensaje se agradece.

Preguntas frecuentes

¿Puede tener descuento también el referido?

No de serie. Un descuento a la cuenta invitada se concede antes de que haya pagado nada, que es justo lo que este diseño evita a propósito: se apoya en una promesa y no en una prueba, y es el único vector de abuso real de todo el sistema. Si aun así lo quiere, aplique su propio cupón en el checkout; aquí nada interferirá.

¿Hace falta el webhook?

Sí, y a diferencia del alta de una suscripción propia, esto no es negociable. El cambio de facturación de un referido afecta a una cuenta que no es la que está delante de la pantalla. Nadie está mirando, así que no hay ninguna actualización perezosa a la que recurrir.

¿Funciona sin Stripe?

Funciona, registra los referidos, los muestra y dice en pantalla que no se aplica ningún descuento. Útil para el desarrollo local y para un despliegue cuya configuración de Stripe no está terminada.

¿Y si mi aplicación no es Next.js?

Las reglas, los códigos, las consultas, la sincronización con Stripe, el manejador del webhook y el reconciliador son TypeScript sin ningún framework dentro. Solo capture.ts, middleware.ts y ui/ son propios de Next, alrededor de un tercio de la carpeta.

¿Por qué no un paquete npm?

Porque las dos cosas que más falta hace instalar son un modelo de Prisma y una página del App Router, y npm no sabe entregar ninguna de las dos sin que usted las copie de todos modos. Una carpeta es honesta al respecto.

El código está en GitHub

Con licencia MIT, y un README que detalla la API completa, los modos de fallo y cómo comprobar, contra una clave real de Stripe, que una factura sale de verdad más baja.