Asterias
Inizia

Il programma di referral di Asterias, open source

Asterias è un prodotto chiuso, ma uno dei suoi pezzi non lo è più. Il programma di referral che gira qui in produzione è stato estratto, riscritto e pubblicato con licenza MIT. Ecco che cosa fa, come lo fa, e le tre trappole che esiste per evitare.

Licenza
MIT
Costruito su
Next.js 15 · Prisma 6 · Stripe 18 · TypeScript
Test unitari
89
Lingue
EN · FR · DE · IT · ES

Che cosa fa questo modulo

Quasi tutti i programmi di referral premiano un’iscrizione. Un’iscrizione non costa nulla da fabbricare, così quei programmi finiscono per essere sfruttati oppure avvolti in regole antifrode che nessuno riesce a spiegare a un cliente in buona fede.

Questo premia il fatturato, e solo finché dura. Ogni referral che diventa cliente pagante toglie una quota all’abbonamento di chi lo ha invitato. Il giorno in cui smette di pagare, quella quota torna sulla fattura successiva.

  • Sarah invita Tom. Tom si iscrive: Sarah non guadagna nulla.
  • Tom avvia una prova gratuita: Sarah continua a non guadagnare nulla.
  • La carta di Tom viene addebitata: Sarah ottiene il 20 % di sconto.
  • Il pagamento di Tom fallisce: Sarah torna a prezzo pieno.
  • Tom paga di nuovo: lo sconto ritorna.

Cinque referral paganti e l’abbonamento di Sarah è gratuito. Il passo e il tetto si impostano in un file di ambiente.

Non c’è nessun salvadanaio di punti, nessun registro di crediti, nessuna coda di pagamenti. Lo sconto è una funzione di quanti referral stanno pagando in questo istante, ricalcolata dal database e scritta su Stripe. È ciò che impedisce allo schermo e alla fattura di dire due cose diverse, perché entrambi leggono la stessa funzione.

Tre trappole, e che cosa ne fa questo modulo

Tre cose vanno storte in quasi ogni programma di referral scritto a mano. Questo modulo è soprattutto la forma che prende evitarle.

La trappolaChe cosa succedeChe cosa fa questo modulo
Un coupon a uso singoloSembra il modo più pulito per rendere vero il «ricalcolato ogni mese»: si attacca un coupon, cade, si attacca il successivo. Lo sconto dipende allora da qualcosa che deve riattaccarlo prima di ogni singola fattura, e ogni finestra in cui non è girato è un addebito a prezzo pieno che nessuno segnala.duration: forever. Lo sconto è uno stato durevole, che il modulo toglie quando decide lui. Una sincronizzazione in ritardo non costa nulla.
Fidarsi del contenuto del webhookStripe non garantisce l’ordine di consegna. Un evento past_due può arrivare dopo l’active che lo aveva sostituito. Scrivere ciò che dice il payload significa spostare lo sconto sull’abbonamento di un terzo, sulla base di uno stato che non è più vero.Ogni evento innesca una rilettura dell’API Stripe. Due elaborazioni concorrenti sono ordinate dal timestamp della loro lettura.
Fidarsi del webhook e bastaUn deploy, un errore 500, un evento che Stripe ha smesso di ritentare: chi ha invitato si ritrova al livello sbagliato, e uno sconto sbagliato somiglia in tutto a uno giusto.Un riconciliatore ricalcola tutti i livelli dal database, ripara lo scarto e dice che cosa ha riparato.

Come si calcola lo sconto

Non esiste, deliberatamente, nessuno «sconto corrente» memorizzato. Il livello è una funzione pura di quanti referral stanno pagando in questo istante.

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

La stessa funzione alimenta la pagina del cliente e la sincronizzazione con Stripe, quindi ciò che gli viene mostrato e ciò che viene addebitato sulla sua carta non possono divergere perché due punti si sono disallineati.

Il cliente lo sente sulla fattura successiva: gli sconti sono scritti con proration_behavior a none, quindi un cambio di livello non genera mai una nota di credito né un addebito immediato per un mese già servito.

Stato StripeConta?Perché
activeIl referral paga.
trialingSecondo l’impostazioneSolo se REFERRAL_COUNT_TRIALING vale true. Leggete la sezione sull’abuso prima di toccarlo.
past_dueNoUn rinnovo è fallito. È questo che vuol dire «lo sconto si ferma al primo mancato pagamento».
unpaidNoLa fattura non è mai stata saldata.
canceledNoL’abbonamento è disdetto.
pausedNoL’abbonamento è in pausa, quindi non viene addebitato nulla.
incompleteNoNessun pagamento è mai andato a buon fine. Lo stesso per un account senza abbonamento o su un piano gratuito.

Configurazione

Tutto vive nell’ambiente ed è validato all’avvio. Una configurazione che non può essere rispettata si rifiuta di partire, invece di fallire il giorno in cui un cliente raggiunge il livello più alto.

VariabilePredefinitoChe cosa fa
REFERRAL_PERCENT_STEP20Percentuale di sconto per ogni referral pagante.
REFERRAL_MAX_REFERRALS5Quanti referral fanno ancora guadagnare uno sconto.
REFERRAL_COUNT_TRIALINGfalseUn referral ancora in prova gratuita conta? Leggete la sezione sull’abuso prima di portarlo a true.
REFERRAL_COUPON_PREFIXreferral_offPrefisso dei coupon creati da questa applicazione. La percentuale viene aggiunta in coda.
REFERRAL_COOKIE_DAYS30Per quanto sopravvive un codice catturato prima dell’iscrizione.
APP_URLnessunoOrigine pubblica. I link di referral sono costruiti a partire da lì.
STRIPE_SECRET_KEYnessunoSenza di essa i referral vengono registrati e mostrati, ma non viene applicato alcuno sconto.
STRIPE_WEBHOOK_SECRETnessunoSenza di esso l’endpoint rifiuta tutto. Servono entrambi per la metà di fatturazione.

Scegliere il passo e il tetto

REFERRAL_PERCENT_STEP moltiplicato per REFERRAL_MAX_REFERRALS deve restare entro 100. Stripe non ha coupon oltre il 100 % di sconto, e un livello che ne chiedesse uno ricadrebbe in silenzio su nessuno sconto, proprio per l’account che se lo è meritato di più.

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

Al 100 % di sconto Stripe emette una fattura a zero e non addebita la carta. La carta smette quindi di essere messa alla prova, e quando il livello torna a essere pagante quel primo addebito reale può fallire su una carta scaduta da mesi. Per questo la pagina di referral indica l’importo della fattura successiva, e per questo 33 × 3 compare nella lista: si ferma al 99 % e mantiene un pagamento vero sull’account.

Provarlo in un minuto

Il repository è un’applicazione Next.js completa: il modulo, un database Postgres, account di dimostrazione e la pagina di referral.

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

Senza chiave Stripe il programma registra i referral, li mostra, non applica alcuno sconto e lo dice a schermo. Una chiave di test e un secret del webhook accendono la metà di fatturazione.

Per vedere entrambi i lati in un solo browser: iscrivetevi, copiate il vostro link, apritelo in una finestra privata e iscrivetevi come qualcun altro, abbonate il secondo account, poi guardate la pagina del primo passare al 20 % con la prossima fattura indicata. Disdite il secondo abbonamento e guardate lo sconto sparire.

Installarlo nella vostra applicazione

Il modulo sta tutto in una cartella, src/referrals. La si copia, e restano tre cose da fare.

I quattro modelli Prisma da aggiungere al vostro schema non hanno alcuna chiave esterna verso la vostra tabella utenti: userId è una stringa opaca, ciò che la vostra applicazione chiama identificativo di account. È questo che ne fa una cartella da copiare invece di una migrazione da fondere. In cambio nulla si cancella a cascata quando un account viene eliminato, da cui forgetUser(userId), da chiamare dal vostro percorso di cancellazione.

1. Attribuire l’account all’iscrizione

attachReferral non solleva mai un’eccezione. Un referral è una cortesia commerciale, un’iscrizione è il mestiere: un codice scaduto, digitato male, autoreferenziale o falsificato restituisce un risultato che potete registrare e ignorare, e l’account viene creato in ogni 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()

Le persone condividono la pagina che le ha convinte, e molto più spesso è la pagina dei prezzi che il modulo di iscrizione. Una riga nel middleware che avete già intercetta ?ref= su tutte le pagine.

2. Catturare il codice ovunque, non solo su /signup

Il codice catturato sopravvive trenta giorni per impostazione predefinita, il tempo che serve a un visitatore per andarsene, pensarci e tornare a creare un account.

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

3. Montare il webhook e marcare il vostro checkout

Il webhook è dove il programma apprende che un referral ha pagato. Il checkout, invece, gli dice a quale account appartiene il cliente Stripe appena creato.

// 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),
})

E la pagina

ReferralPage riceve uno userId e non stabilisce mai da sé chi è collegato, di proposito. L’autenticazione è la vostra: un componente che risolve la sessione da solo è un componente che si può montare su una rotta dove nessuno ha controllato.

// 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' }}
  />
}

L’abuso, e perché questo disegno gli resiste

La riga più importante di questo modulo è che una prova gratuita non conta.

Se le prove contassero, chiunque aprirebbe cinque account, avvierebbe cinque prove, prenderebbe il 100 % di sconto sul proprio abbonamento e le cancellerebbe tutte e cinque prima che una carta venga toccata. Il programma pagherebbe per nulla.

Escluse le prove, guadagnare uno sconto richiede abbonamenti davvero pagati. Per fingere cinque referral paganti bisogna pagare davvero cinque abbonamenti per renderne uno gratuito. È l’economia a fare da vigilanza, ed è per questo che qui non c’è alcun punteggio antifrode da regolare né da spiegare in un ticket di assistenza.

Quello che resta a voi: limitare la frequenza sulla vostra rotta di iscrizione. Questo modulo non vede il vostro traffico, e la creazione di account è il vostro endpoint.

L’attaccoPerché fallisce
Invitare sé stessiresolveReferrer rifiuta quando il proprietario del codice è l’account che sta nascendo.
Due referrer per uno stesso accountreferredUserId è unico. Vince il primo, per sempre.
Falsificare il cookie o il valore di ?ref=Entrambi sono controllati dal visitatore e a nessuno dei due si crede: il codice viene cercato nel database al momento dell’iscrizione. Falsificarlo non fa altro che dare uno sconto all’account indicato, quindi non c’è nulla da rubare.
Rivendicare referral in modo retroattivoattachReferral rifiuta un account che è già stato cliente.
Rigiocare un webhook StripeOgni id di evento viene registrato prima dell’elaborazione, e la chiave primaria scarta la ripetizione.
Leggere i referral di un altro accountOgni query è vincolata allo userId che passate. Passatene uno autenticato.

Esercizio

Quattro comandi, di cui due contano davvero.

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 parla solo quando ha fatto qualcosa. Una scansione che scrive un log a ogni passaggio tranquillo abitua tutti a ignorarla, e poi si perde anche il giorno rumoroso. Se ripara venti sconti al giorno, qualcosa a monte è rotto e dovete saperlo.

La diagnosi, dal server

doctor risponde alla domanda che in un browser non ha risposta: un secret del webhook mancante, una chiave che punta all’account Stripe sbagliato, un coupon con la durata sbagliata. Visti da fuori, questi tre casi somigliano esattamente a un programma in cui nessuno ha ancora invitato nessuno.

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.

Che cosa è testato, e che cosa no

89 test unitari, senza database e senza rete: l’aritmetica dei livelli e i suoi limiti, la generazione e l’escape dei codici, la validazione della configurazione, la corrispondenza degli stati, le protezioni dell’attribuzione, firma, idempotenza e smistamento del webhook, e la sincronizzazione dello sconto, compreso il suo percorso di errore.

Otto di questi sono test sul filo: l’SDK Stripe vero, parametri veri serializzati, contro un registratore locale. Verificano i byte e non un mock: duration=forever sul coupon, discounts[0][coupon] quando si applica, discounts vuoto quando si rimuove, proration_behavior=none in entrambi i casi. Sono le tre cose che in un’integrazione Stripe hanno più probabilità di essere sbagliate in silenzio, e le più invisibili a un test mockato.

Ciò che nulla di tutto questo copre: come risponde davvero Stripe, e se una fattura vera esce più bassa. Servono una chiave di test e cinque minuti, e il repository indica i passaggi.

Cinque lingue

Inglese, francese, tedesco, italiano, spagnolo. L’inglese è la lingua predefinita e il ripiego per tutto ciò che non è riconosciuto; fr-CA si risolve in fr. I dizionari sono tipizzati per lingua, quindi aggiungere una lingua, o anche solo una stringa, non compila finché tutte e cinque non ce l’hanno. Un test verifica inoltre che nessuna traduzione perda un segnaposto.

Che cosa fa girare qui

Non è un’astrazione da fine settimana. Fa girare il programma di referral di Asterias, il software per le recensioni Google sul cui sito vi trovate. Un commerciante che invita un collega vede il proprio abbonamento scendere del 20 % appena quel collega paga davvero, e risalire se smette.

La versione pubblicata non è una copia del codice di qui: è una riscrittura, e corregge due difetti reali trovati mentre veniva estratta. Se gestite qualcosa di simile, valgono dieci minuti del vostro tempo.

Licenza

MIT. Copiate la cartella, modificatela, vendete quello che ne fate. Se la mettete in produzione, un messaggio fa piacere.

Domande frequenti

Anche il referral può avere uno sconto?

Non così com’è. Uno sconto concesso all’account invitato arriva prima che abbia pagato qualcosa, ed è esattamente ciò che questo disegno evita di proposito: si fonda su una promessa e non su una prova, ed è l’unico vero vettore di abuso dell’intero sistema. Se lo volete comunque, applicate il vostro coupon al checkout: qui nulla interferisce.

Serve il webhook?

Sì, e a differenza dell’attivazione di un abbonamento questo non è negoziabile. Il cambio di fatturazione di un referral riguarda un account che non è quello davanti allo schermo. Nessuno sta guardando, quindi non c’è alcun aggiornamento pigro su cui ripiegare.

Funziona senza Stripe?

Gira, registra i referral, li mostra e dice a schermo che non viene applicato alcuno sconto. Utile per lo sviluppo locale e per un deploy la cui configurazione Stripe non è ancora finita.

E se la mia applicazione non è in Next.js?

Regole, codici, query, sincronizzazione Stripe, gestore del webhook e riconciliatore sono TypeScript puro, senza framework dentro. Solo capture.ts, middleware.ts e ui/ sono specifici di Next, circa un terzo della cartella.

Perché non un pacchetto npm?

Perché le due cose che serve installare di più sono un modello Prisma e una pagina App Router, e npm non sa consegnare né l’una né l’altra senza che le copiate comunque. Una cartella è onesta su questo.

Il codice è su GitHub

Con licenza MIT, e un README che descrive l’API completa, i modi di guasto e come verificare, contro una vera chiave Stripe, che una fattura esca davvero più bassa.