02Criteri5 min

El primer que trec de qualsevol plantilla és Stripe

Arrenco gairebé tots els projectes esborrant el checkout, el portal i el webhook. No és despistament: és separar la infraestructura que sempre necessitaré de la que encara no toca.

Per Salva Castells

Imatge de portada: MarkusHagenlocher, CC BY-SA 3.0.
En aquest post

Vaig mirar l'historial d'una app de control d'hores que vaig escriure fa un any. El primer commit va ser a les 15:48. A les 16:55, seixanta-set minuts després, el segon commit rellevant es deia "remove Stripe API routes for checkout, portal and webhook". Tres fitxers fora: 68 línies de checkout, 48 de portal, 222 de webhook. Encara no havia escrit ni la pantalla principal.

No va ser un rampell. Aquest és el patró: clono el boilerplate, i el primer que faig és apagar els pagaments.

El reflex: clonar i esborrar

El boilerplate que faig servir porta Stripe muntat de fàbrica. I està bé que el porti: quan cal, tenir-ho resolt estalvia un dia. El problema és que ve encès per defecte, i encès vol dir que arrossega coses.

En una plantilla interna que vaig arrencar aquesta primavera, el commit de "remove Stripe integration and related billing features" va ser el segon del repositori, el mateix dia que l'init project. El diff no tocava només rutes. Es va endur per davant:

  • La taula user_billing sencera. customer_id, price_id, sub_credits, otp_credits, initial_sub_date, last_otp_date, has_sub, cancel_next_month. Nou columnes i un índex sobre user_id per gestionar un cobrament que no existia.
  • Els priceId hardcodejats a la config. Dos plans d'exemple amb identificadors reals de Stripe (price_1R3Rhw...) copiats d'un altre projecte. Això són bombes de rellotgeria: ningú no els esborra, algú els desplega.
  • Cinc variables d'entorn. USE_STRIPE, NEXT_PUBLIC_USE_STRIPE, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Cinc entrades més al .env.example que algú s'haurà de preguntar si calen.
  • La primera migració. Es va esborrar i es va regenerar, perquè la taula de billing hi era a dins.

I el ConfigFile, que declarava [key: string]: string | string[] | PricingConfig | PricingConfig[], va passar a ser string | string[]. Un tipus que ha deixat d'haver de contemplar plans de preus és un tipus amb què es treballa millor durant els tres mesos següents.

Per què els pagaments són el que menys em diuen

Un webhook de Stripe no és una ruta més. Són 222 línies que verifiquen signatura, distingeixen checkout.session.completed d'invoice.paid i de customer.subscription.deleted, i escriuen a la base de dades des de fora de la teva sessió. Això s'ha de provar amb el CLI reenviant esdeveniments, s'ha de fer idempotent, i cal decidir què passa quan arriba dues vegades el mateix esdeveniment.

Tota aquesta feina és real. Només que respon a una pregunta que encara no m'he guanyat: quant cobro? I aquesta pregunta va després d'una altra molt més barata de contestar: això serveix a algú?

Mentre no sàpiga la segona, el webhook és codi que mantinc i no exercito. El dia que sí que calgui, el torno a posar en una tarda, i aquesta vegada amb el model de preus decidit en comptes d'amb dos plans d'exemple heretats.

Què més cau en aquella primera poda

Stripe no viatja sol. En un MVP de tres dies per gestionar incidències, el commit número dos del repo es diu, literalment, "remove useless things". Dues hores i onze minuts després del primer. Es va endur el panell d'admin complet (client-page.tsx amb 250 línies, la taula d'usuaris amb 380, el gràfic amb 114), les rutes de feedback, la taula de feedback i el seu schema. Les rutes de Stripe no les va esborrar: les va reanomenar a .txt. route.ts a route.txt. Deixen de compilar, deixen d'existir per a Next, i continuen allà si algun dia calen.

L'endemà, en aquell mateix projecte, va caure el magic link. La pàgina d'auth tenia dos camins: Google i email amb codi de verificació, amb el seu nonce, el seu magicLinkSent, el seu magicLinkCode, el seu handleMagicLinkCode. El commit es diu "eliminar magic link (solo Google)" i s'endú 116 línies de la pàgina. En una app interna per a una empresa on tothom té compte de Google, el segon camí només afegia estats per mantenir.

En una altra plantilla, fa uns mesos, el mateix tall li va tocar a l'admin: fora /admin, fora /account, fora hasAdminRole(), fora ADMIN_EMAILS del .env.example. I fora quatre columnes de l'schema d'usuaris que només servien al panell: role, banned, ban_reason, ban_expires. Més impersonated_by a la taula de sessions.

Aquesta última és la que més m'agrada com a exemple. Un camp impersonated_by a sessions no és una comoditat: és una decisió de seguretat que algú ha d'entendre abans de tocar l'autenticació. Arrossegar-lo "per si de cas" en un projecte sense panell d'admin és deixar una porta amb el pany posat i sense paret al voltant.

On sí que el vaig deixar posat

No és una regla contra els pagaments. És una regla sobre quan.

En un SaaS de generació d'imatges, Stripe va entrar aviat i s'hi va quedar: checkout habilitat, i sis dies després el webhook actiu i els crèdits diaris funcionant. Allà el cobrament no era una hipòtesi de negoci, era contenció de costos: cada generació costa diners reals en el moment en què algú prem el botó. Sense crèdits no hi ha producte, hi ha una factura.

El mateix en una API de transcripció: el billing i els límits del pla gratuït es van escriure quan encara s'estava muntant el compte d'usuari, perquè el minut d'àudio processat té preu des de la primera petició.

El criteri es veu clar posat al costat: si cada ús et costa diners, el cobrament és infraestructura i va des del dia u. Si el cost marginal és zero, el cobrament és producte i va quan sàpigues què vens. Una eina de control d'hores, un gestor d'incidències intern o una plantilla no gasten res per usuari. Un model d'imatges sí.

El que queda després de podar

Després d'aquella primera hora, el que sobreviu a tots els projectes és sempre el mateix: auth amb un sol proveïdor, la base de dades amb les taules del domini i res més, i les rutes que l'app necessita per fer el que li toca. Això és el que anomeno infraestructura. Tota la resta és una aposta sobre el futur del producte, i les apostes es paguen en manteniment encara que no guanyis.

Una plantilla completa t'estalvia la primera tarda. Esborrar el que no toca t'estalvia els tres mesos següents.