02Criterio5 min

La prima cosa che tolgo da qualunque template è Stripe

Inizio quasi tutti i progetti cancellando il checkout, il portale e il webhook. Non è distrazione: è separare l'infrastruttura che mi servirà sempre da quella che non tocca ancora.

Di Salva Castells

Immagine di copertina: MarkusHagenlocher, CC BY-SA 3.0.
In questo post

Ho guardato la cronologia di un'app di rilevamento ore che ho scritto un anno fa. Il primo commit era alle 15:48. Alle 16:55, sessantasette minuti dopo, il secondo commit rilevante si chiamava "remove Stripe API routes for checkout, portal and webhook". Tre file fuori: 68 righe di checkout, 48 di portale, 222 di webhook. Non avevo ancora scritto nemmeno la schermata principale.

Non è stato un raptus. Il pattern è quello: clono il boilerplate, e la prima cosa che faccio è spegnere i pagamenti.

Il riflesso: clonare e cancellare

Il boilerplate che uso porta Stripe già montato. Ed è giusto che ce l'abbia: quando serve, averlo risolto ti fa risparmiare una giornata. Il problema è che arriva acceso di default, e acceso vuol dire che si trascina dietro delle cose.

In un template interno che ho avviato questa primavera, il commit "remove Stripe integration and related billing features" è stato il secondo del repository, lo stesso giorno dell'init project. Il diff non toccava solo le rotte. Si è portato via:

  • L'intera tabella user_billing. customer_id, price_id, sub_credits, otp_credits, initial_sub_date, last_otp_date, has_sub, cancel_next_month. Nove colonne e un indice su user_id per gestire un incasso che non esisteva.
  • I priceId hardcoded nella config. Due piani di esempio con identificatori Stripe veri (price_1R3Rhw...) copiati da un altro progetto. Quelle sono bombe a orologeria: nessuno li cancella, qualcuno li rilascia.
  • Cinque variabili d'ambiente. USE_STRIPE, NEXT_PUBLIC_USE_STRIPE, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Cinque voci in più nell'.env.example su cui qualcuno dovrà chiedersi se servono.
  • La prima migrazione. È stata cancellata e rigenerata, perché la tabella di billing stava dentro.

E il ConfigFile, che dichiarava [key: string]: string | string[] | PricingConfig | PricingConfig[], è diventato string | string[]. Un tipo che non deve più contemplare piani di prezzo è un tipo con cui si lavora meglio nei tre mesi successivi.

Perché i pagamenti sono quello che mi dice meno

Un webhook di Stripe non è una rotta qualsiasi. Sono 222 righe che verificano la firma, distinguono checkout.session.completed da invoice.paid da customer.subscription.deleted, e scrivono nel database da fuori della tua sessione. Va provato con la CLI che rimanda eventi, va reso idempotente, e bisogna decidere cosa succede quando lo stesso evento arriva due volte.

Tutto quel lavoro è reale. Solo che risponde a una domanda che non mi sono ancora guadagnato: quanto faccio pagare? E quella domanda viene dopo un'altra molto più economica da rispondere: questa cosa serve a qualcuno?

Finché non so la seconda, il webhook è codice che mantengo e non esercito. Il giorno in cui servirà davvero, lo rimetto in un pomeriggio, e stavolta con il modello di prezzo deciso invece che con due piani di esempio ereditati.

Cos'altro cade in quella prima potatura

Stripe non viaggia da solo. In un MVP di tre giorni per gestire segnalazioni, il commit numero due del repo si chiama, letteralmente, "remove useless things". Due ore e undici minuti dopo il primo. Si è portato via il pannello di admin completo (client-page.tsx con 250 righe, la tabella utenti con 380, il grafico con 114), le rotte di feedback, la tabella di feedback e il suo schema. Le rotte di Stripe non le ha cancellate: le ha rinominate in .txt. route.ts in route.txt. Smettono di compilare, smettono di esistere per Next, e restano lì se un giorno servono.

Il giorno dopo, in quello stesso progetto, è caduto il magic link. La pagina di auth aveva due strade: Google ed email con codice di verifica, con il suo nonce, il suo magicLinkSent, il suo magicLinkCode, il suo handleMagicLinkCode. Il commit si chiama "eliminar magic link (solo Google)" e si porta via 116 righe della pagina. In un'app interna per un'azienda dove tutti hanno un account Google, la seconda strada aggiungeva solo stati da mantenere.

In un altro template, qualche mese fa, lo stesso taglio è toccato all'admin: fuori /admin, fuori /account, fuori hasAdminRole(), fuori ADMIN_EMAILS dall'.env.example. E fuori quattro colonne dallo schema utenti che servivano solo al pannello: role, banned, ban_reason, ban_expires. Più impersonated_by nella tabella delle sessioni.

Quest'ultima è quella che mi piace di più come esempio. Un campo impersonated_by in sessions non è una comodità: è una decisione di sicurezza che qualcuno deve capire prima di toccare l'autenticazione. Trascinarselo dietro "per sicurezza" in un progetto senza pannello di admin è lasciare una porta con la serratura messa e senza il muro intorno.

Dove invece l'ho lasciato

Non è una regola contro i pagamenti. È una regola su quando.

In un SaaS di generazione di immagini, Stripe è entrato presto ed è rimasto: checkout abilitato, e sei giorni dopo il webhook attivo e i crediti giornalieri funzionanti. Lì l'incasso non era un'ipotesi di business, era contenimento dei costi: ogni generazione costa soldi veri nel momento in cui qualcuno preme il bottone. Senza crediti non c'è un prodotto, c'è una fattura.

Lo stesso in un'API di trascrizione: il billing e i limiti del piano gratuito sono stati scritti mentre si stava ancora montando l'account utente, perché il minuto di audio elaborato ha un prezzo dalla prima richiesta.

Il criterio si vede chiaro messo di fianco: se ogni uso ti costa soldi, l'incasso è infrastruttura e va dal giorno uno. Se il costo marginale è zero, l'incasso è prodotto e va quando saprai cosa vendi. Uno strumento di rilevamento ore, un gestore di segnalazioni interno o un template non spendono niente per utente. Un modello di immagini sì.

Cosa resta dopo la potatura

Dopo quella prima ora, quello che sopravvive in tutti i progetti è sempre lo stesso: auth con un solo provider, il database con le tabelle del dominio e niente altro, e le rotte che l'app serve per fare il suo lavoro. Questo è quello che chiamo infrastruttura. Tutto il resto è una scommessa sul futuro del prodotto, e le scommesse si pagano in manutenzione anche quando non vinci.

Un template completo ti fa risparmiare il primo pomeriggio. Cancellare quello che non tocca ti fa risparmiare i tre mesi successivi.