Salta al contenuto
Tutti gli articoli

Webhook idempotenti per Stripe e Lemon Squeezy in Next.js

Come verificare le firme grezze dei webhook, deduplicare le consegne ripetute al confine del database ed evitare che abbonamenti sovrapposti revochino l'accesso a pagamento.

il team supastack2 min di lettura

How do I handle Stripe and Lemon Squeezy webhooks idempotently in Next.js?

It depends on your billing providers, but the core architecture is identical: verify the raw payload signature before parsing JSON, normalize provider events into a shared internal contract, and enforce idempotency at the database boundary using the provider's event or order ID. Never grant product entitlements from a client-side checkout redirect; let the verified webhook transactionally update subscription state and entitlements once. supastack prewires this pipeline for Stripe, Lemon Squeezy, and mobile RevenueCat, though single-plan apps can start with a simpler handler.

Verifica il payload grezzo prima di toccare il JSON

Ogni fornitore di pagamenti firma l'esatta sequenza di byte che invia sulla rete. Se un route handler analizza await request.json() e riassembla l'oggetto prima di controllare la firma, le differenze nell'ordine delle chiavi o negli spazi renderanno non validi i webhook legittimi — o spingeranno gli sviluppatori a saltare la verifica in staging.

Leggi il corpo grezzo una sola volta con await request.text() nel tuo handler POST dell'App Router di Next.js, verifica la firma crittografica rispetto al segreto riservato al server e rifiuta qualsiasi errore con HTTP 400 prima di eseguire una singola query sul database. La guida al modulo di fatturazione mostra come supastack isola i controlli delle firme dietro un unico punto di ingresso BFF.

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  const event = stripe.webhooks.constructEvent(
    rawBody,
    signature,
    env.STRIPE_WEBHOOK_SECRET,
  );
  await handleNormalizedBillingEvent(event);
  return Response.json({ received: true });
}
  • Leggi il corpo grezzo della richiesta con request.text() — non serializzare mai di nuovo un JSON già analizzato.
  • Conserva STRIPE_WEBHOOK_SECRET e i segreti di firma di Lemon Squeezy esclusivamente nella configurazione server.
  • Usa il confronto a tempo costante (crypto.timingSafeEqual) quando verifichi manualmente le firme HMAC.

Deduplica al confine del database, non in memoria

Stripe e Lemon Squeezy ritentano l'invio dei webhook ogni volta che un deploy si riavvia, una funzione serverless va in timeout o un salto di rete perde la risposta. I set locali al processo o le chiavi di cache a breve durata svaniscono tra le istanze e non impediranno a due tentativi concorrenti di concedere licenze duplicate o inviare due email di ricevuta.

Registra l'ID evento del fornitore o lo stato di saldo dell'ordine all'interno della stessa transazione Postgres che modifica l'accesso all'abbonamento. Un INSERT ... ON CONFLICT DO NOTHING atomico o un aggiornamento condizionale su status != 'paid' trasforma una ripetizione concorrente in una semplice operazione senza effetto. Quando progetti i piani attivati da quei webhook, il generatore di pagine prezzi SaaS aiuta a modellare in anticipo livelli e regole per postazione.

const settled = await db.transaction(async (tx) => {
  const [inserted] = await tx
    .insert(billingWebhookEvents)
    .values({ providerEventId: event.id, provider: "stripe" })
    .onConflictDoNothing({ target: billingWebhookEvents.providerEventId })
    .returning({ id: billingWebhookEvents.id });

  if (!inserted) return false; // Already processed
  await applyWorkspaceEntitlement(tx, event);
  return true;
});

Gestisci eventi fuori ordine e abbonamenti sovrapposti

Non è garantito che i webhook arrivino nell'ordine in cui l'utente ha fatto clic. customer.subscription.updated può arrivare prima di checkout.session.completed e un cliente che apre per errore due schede di checkout può detenere brevemente due abbonamenti nello stesso workspace.

Associa disdette ed errori di pagamento allo specifico ID di abbonamento che ha generato l'evento. Prima di revocare il diritto a pagamento di un workspace, verifica se un altro abbonamento attivo copre ancora quel workspace — altrimenti annullare un abbonamento duplicato rimuove silenziosamente l'accesso per cui il cliente sta ancora pagando.

  • Indicizza lo stato dell'abbonamento per providerSubscriptionId, non solo per workspaceId.
  • Mantieni attivo il diritto a pagamento di un workspace finché almeno un abbonamento è active o trialing.
  • Verifica che l'importo pagato e la valuta corrispondano allo SKU previsto prima di evadere ordini una tantum.

Domande frequenti sui webhook di fatturazione SaaS

Queste domande emergono ogni volta che un team porta la fatturazione dai test in sandbox al traffico di produzione.

  • Posso sbloccare le funzionalità a pagamento sul redirect di successo del checkout? — No. Gli URL di redirect possono essere salvati nei preferiti, condivisi o visitati prima che i metodi di pagamento asincroni vengano saldati. Usa la pagina di successo per interrogare lo stato del server mentre il webhook verificato concede l'accesso.
  • Quale codice di stato deve restituire un webhook duplicato? — Restituisci 200 OK non appena il database conferma che l'evento è già stato elaborato. Restituire 4xx o 5xx dice al fornitore di continuare a riprovare.
  • Come testo i tentativi ripetuti dei webhook in locale? — Scrivi test unitari deterministici che inviano due volte lo stesso identico payload firmato e verifica che venga prodotta una sola licenza, un solo diritto o una sola email transazionale.
  • Come si inseriscono gli acquisti in-app mobili nello stesso modello? — Instrada le notifiche server di RevenueCat attraverso la stessa tabella idempotente dei diritti in modo che gli acquisti web e mobili non si sovrascrivano mai — consulta la checklist di lancio per i controlli pre-rilascio.

Domande frequenti

14 domande

Cos'è supastack?

supastack è una base modulare per costruire prodotti SaaS con AI. Riunisce l'infrastruttura comune così puoi concentrarti sulle funzionalità proprie del tuo prodotto.

Supporta web e mobile?

Sì. supastack include app Next.js ed Expo come punti di partenza per esperienze web e mobile.

Quale autenticazione è inclusa?

Better Auth copre email/password, OTP e login social. I workspace personali vengono provisionati con l'account così i record restano delimitati fin dal primo giorno.

Come è supportata l'AI?

supastack include AI multi-provider configurabile, tracciamento dell'uso, budget e supporto cifrato per le proprie chiavi (BYOK).

Come vengono gestiti gli abbonamenti?

Per i prodotti costruiti sul kit, fatturazione Stripe, abbonamenti e permessi formano la base dell'accesso a pagamento. I segreti dei provider restano lato server.

Dove vive il codice specifico del mio prodotto?

Le funzionalità specifiche del prodotto restano in un codebase separato, mentre il prodotto configura e integra i moduli base di supastack.

Come funziona il prezzo di supastack?

Il kit è un acquisto unico con aggiornamenti a vita: Stack singolo a 249 $ — a scelta tra i repo Next.js (web), Expo (iOS e Android) o Tauri (macOS e Windows) —, Doppio stack a 398 $ con due repo a scelta, o Pacchetto completo a 399 $ con tutti e tre i repo e il monorepo completo, più 75 $ per posto sviluppatore aggiuntivo. Nessun abbonamento al kit. Il checkout su questo sito consegna la tua chiave di licenza via email, con accesso al repo tramite invito GitHub.

Come delimitano i workspace i miei dati?

I record utente sono delimitati da workspaceId. Identità, permessi, uso AI, eventi di fatturazione e voci di audit restano dentro il confine del workspace personale.

Come funzionano i job in background?

Trigger.dev è il runtime di job esplicito. Il contratto JobRunner promette solo enqueue, status e cancel — la semantica di workflow resta specifica ed esplicita di Trigger.dev.

In cosa differisce l'email dall'outreach?

L'email transazionale copre i flussi di prodotto come gli avvisi di completamento delle attività. L'outreach a freddo e le campagne appartengono al prodotto, fuori dal modulo email di questo kit.

Come funzionano gli acquisti su mobile?

La versione mobile include acquisti nativi e ripristino tramite RevenueCat, con accesso verificato dal server. Configura gli account degli store e completa i test sui dispositivi prima della pubblicazione. I pagamenti web restano separati.

Dove risiedono le chiavi dei provider AI?

Le credenziali BYOK sono cifrate e restano lato server. Le chiavi vengono sanificate nei log; uso e budget restano visibili a livello di workspace.

Quali metodi di pagamento accetta il checkout?

Il checkout ospitato da Stripe accetta le carte e può offrire anche Alipay e WeChat Pay una volta attivati i wallet sull'account Stripe — utile per gli acquirenti in Cina. Le autorizzazioni dei wallet vengono confermate in modo asincrono, quindi la pagina della licenza si aggiorna non appena il pagamento va a buon fine.

Posso ottenere un rimborso?

Una volta ottenuto l'accesso al repository, supastack è tuo per sempre, quindi non può essere rimborsato. Ma non preoccuparti: ricevi il codice sorgente completo con aggiornamenti a vita per creare e lanciare prodotti illimitati per te o per i tuoi clienti. L'unica condizione è che non puoi rivendere il codice né ridistribuire il repository come starter kit o modello. Un'eccezione è il cashback di lancio — lancia un prodotto live creato con supastack, condividilo e raggiungi 5.000 visualizzazioni organiche o 50 Mi piace organici entro 60 giorni per ricevere il 50% di quanto hai pagato; vedi i termini.

IT