02Jugement5 min

La première chose que je retire de n'importe quel template, c'est Stripe

Je démarre presque tous mes projets en supprimant le checkout, le portail et le webhook. Ce n'est pas de la distraction : c'est séparer l'infrastructure dont j'aurai toujours besoin de celle dont ce n'est pas encore le moment.

Par Salva Castells

Image de couverture : MarkusHagenlocher, CC BY-SA 3.0.
Dans cet article

J'ai regardé l'historique d'une application de suivi des heures que j'ai écrite il y a un an. Le premier commit était à 15 h 48. À 16 h 55, soixante-sept minutes plus tard, le deuxième commit significatif s'appelait « remove Stripe API routes for checkout, portal and webhook ». Trois fichiers dehors : 68 lignes de checkout, 48 de portail, 222 de webhook. Je n'avais même pas encore écrit l'écran principal.

Ce n'était pas un coup de sang. C'est le schéma : je clone le boilerplate, et la première chose que je fais, c'est éteindre les paiements.

Le réflexe : cloner et supprimer

Le boilerplate que j'utilise embarque Stripe d'usine. Et c'est bien qu'il l'embarque : quand le besoin arrive, l'avoir déjà réglé fait gagner une journée. Le problème, c'est qu'il arrive allumé par défaut, et allumé signifie qu'il traîne des choses derrière lui.

Dans un template interne que j'ai démarré ce printemps, le commit « remove Stripe integration and related billing features » était le deuxième du dépôt, le même jour que l'init project. Le diff ne touchait pas seulement à des routes. Il a emporté :

  • Toute la table user_billing. customer_id, price_id, sub_credits, otp_credits, initial_sub_date, last_otp_date, has_sub, cancel_next_month. Neuf colonnes et un index sur user_id pour gérer un encaissement qui n'existait pas.
  • Les priceId en dur dans la config. Deux plans d'exemple avec de vrais identifiants Stripe (price_1R3Rhw...) copiés d'un autre projet. Ce sont des bombes à retardement : personne ne les supprime, quelqu'un les déploie.
  • Cinq variables d'environnement. USE_STRIPE, NEXT_PUBLIC_USE_STRIPE, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Cinq entrées de plus dans le .env.example dont quelqu'un devra se demander si elles sont nécessaires.
  • La première migration. Elle a été supprimée et régénérée, parce que la table de billing était dedans.

Et le ConfigFile, qui déclarait [key: string]: string | string[] | PricingConfig | PricingConfig[], est devenu string | string[]. Un type qui n'a plus à envisager des plans tarifaires est un type avec lequel on travaille mieux pendant les trois mois qui suivent.

Pourquoi les paiements sont ce qui m'apprend le moins

Un webhook Stripe n'est pas une route de plus. Ce sont 222 lignes qui vérifient une signature, distinguent checkout.session.completed d'invoice.paid et de customer.subscription.deleted, et écrivent en base de données depuis l'extérieur de votre session. Il faut le tester avec le CLI en réémettant des événements, il faut le rendre idempotent, et il faut décider ce qui se passe quand le même événement arrive deux fois.

Tout ce travail est réel. Sauf qu'il répond à une question que je n'ai pas encore méritée : combien je facture ? Et cette question vient après une autre, beaucoup moins chère à répondre : est-ce que ça sert à quelqu'un ?

Tant que je ne sais pas la seconde, le webhook est du code que je maintiens et que je n'exerce pas. Le jour où j'en aurai vraiment besoin, je le remets en un après-midi, et cette fois avec le modèle tarifaire décidé plutôt qu'avec deux plans d'exemple hérités.

Ce qui tombe aussi lors de ce premier élagage

Stripe ne voyage pas seul. Dans un MVP de trois jours pour gérer des incidents, le commit numéro deux du dépôt s'appelle, littéralement, « remove useless things ». Deux heures et onze minutes après le premier. Il a emporté tout le panneau d'admin (client-page.tsx avec 250 lignes, la table des utilisateurs avec 380, le graphique avec 114), les routes de feedback, la table de feedback et son schéma. Les routes Stripe, il ne les a pas supprimées : il les a renommées en .txt. route.ts en route.txt. Elles ne compilent plus, elles n'existent plus pour Next, et elles sont toujours là si un jour elles servent.

Le lendemain, dans ce même projet, c'est le magic link qui est tombé. La page d'auth avait deux chemins : Google et e-mail avec code de vérification, avec son nonce, son magicLinkSent, son magicLinkCode, son handleMagicLinkCode. Le commit s'appelle « eliminar magic link (solo Google) » et emporte 116 lignes de la page. Dans une application interne pour une entreprise où tout le monde a un compte Google, le second chemin n'ajoutait que des états à maintenir.

Dans un autre template, il y a quelques mois, la même coupe a touché l'admin : dehors /admin, dehors /account, dehors hasAdminRole(), dehors ADMIN_EMAILS du .env.example. Et dehors quatre colonnes du schéma utilisateurs qui ne servaient qu'au panneau : role, banned, ban_reason, ban_expires. Plus impersonated_by dans la table des sessions.

C'est cette dernière que je préfère comme exemple. Un champ impersonated_by dans sessions n'est pas un confort : c'est une décision de sécurité que quelqu'un doit comprendre avant de toucher à l'authentification. Le traîner « au cas où » dans un projet sans panneau d'admin, c'est laisser une porte verrouillée sans mur autour.

Là où je l'ai bel et bien gardé

Ce n'est pas une règle contre les paiements. C'est une règle sur le moment.

Dans un SaaS de génération d'images, Stripe est entré tôt et est resté : checkout activé, et six jours plus tard le webhook actif et les crédits quotidiens en marche. Là, l'encaissement n'était pas une hypothèse commerciale, c'était de la maîtrise des coûts : chaque génération coûte de l'argent réel au moment où quelqu'un appuie sur le bouton. Sans crédits il n'y a pas de produit, il y a une facture.

Pareil dans une API de transcription : le billing et les limites du plan gratuit ont été écrits alors que le compte utilisateur se montait encore, parce que la minute d'audio traitée a un prix dès la première requête.

Le critère devient clair quand on met les deux côte à côte : si chaque usage vous coûte de l'argent, l'encaissement est de l'infrastructure et il arrive dès le jour un. Si le coût marginal est nul, l'encaissement est du produit et il arrive quand vous saurez ce que vous vendez. Un outil de suivi des heures, un gestionnaire d'incidents interne ou un template ne dépensent rien par utilisateur. Un modèle d'images, si.

Ce qui reste après l'élagage

Après cette première heure, ce qui survit dans tous les projets est toujours la même chose : l'auth avec un seul fournisseur, la base de données avec les tables du domaine et rien d'autre, et les routes dont l'application a besoin pour faire son travail. C'est ça que j'appelle l'infrastructure. Tout le reste est un pari sur l'avenir du produit, et les paris se paient en maintenance même quand vous perdez.

Un template complet vous fait gagner le premier après-midi. Supprimer ce qui n'a pas lieu d'être vous fait gagner les trois mois suivants.