02Oordeel5 min

Het eerste wat ik uit elke template haal is Stripe

Ik begin bijna elk project met het verwijderen van de checkout, het portaal en de webhook. Geen verstrooidheid: het is de infrastructuur die ik altijd nodig ga hebben scheiden van wat nog niet aan de beurt is.

Door Salva Castells

Omslagafbeelding: MarkusHagenlocher, CC BY-SA 3.0.
In dit bericht

Ik keek naar de historie van een urenregistratie-app die ik een jaar geleden schreef. De eerste commit was om 15:48. Om 16:55, zevenenzestig minuten later, heette de tweede relevante commit "remove Stripe API routes for checkout, portal and webhook". Drie bestanden eruit: 68 regels checkout, 48 portaal, 222 webhook. Ik had nog niet eens het hoofdscherm geschreven.

Het was geen opwelling. Dat is het patroon: ik kloon de boilerplate, en het eerste wat ik doe is de betalingen uitzetten.

De reflex: klonen en verwijderen

De boilerplate die ik gebruik heeft Stripe standaard ingebouwd. En het is goed dat hij dat heeft: als het nodig is, scheelt het een dag dat het al geregeld is. Het probleem is dat het standaard aan staat, en aan betekent dat het dingen meesleept.

In een interne template die ik dit voorjaar startte was de commit "remove Stripe integration and related billing features" de tweede van de repository, dezelfde dag als de init project. De diff raakte niet alleen routes. Weg gingen:

  • De hele tabel user_billing. customer_id, price_id, sub_credits, otp_credits, initial_sub_date, last_otp_date, has_sub, cancel_next_month. Negen kolommen en een index op user_id om een betaling te beheren die niet bestond.
  • De hardcoded priceId's in de config. Twee voorbeeldplannen met echte Stripe-identifiers (price_1R3Rhw...) gekopieerd uit een ander project. Dat zijn tijdbommen: niemand verwijdert ze, iemand deployt ze.
  • Vijf omgevingsvariabelen. USE_STRIPE, NEXT_PUBLIC_USE_STRIPE, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Vijf regels extra in de .env.example waarvan iemand zich zal moeten afvragen of ze nodig zijn.
  • De eerste migratie. Die werd verwijderd en opnieuw gegenereerd, want de billingtabel zat erin.

En de ConfigFile, die [key: string]: string | string[] | PricingConfig | PricingConfig[] declareerde, werd string | string[]. Een type dat geen rekening meer hoeft te houden met prijsplannen is een type waarmee je de volgende drie maanden prettiger werkt.

Waarom betalingen me het minst zeggen

Een Stripe-webhook is niet zomaar een route. Het zijn 222 regels die de handtekening verifiëren, checkout.session.completed onderscheiden van invoice.paid van customer.subscription.deleted, en van buiten jouw sessie in de database schrijven. Dat moet je testen met de CLI die events doorstuurt, het moet idempotent zijn, en je moet beslissen wat er gebeurt als hetzelfde event twee keer binnenkomt.

Al dat werk is echt. Alleen beantwoordt het een vraag die ik nog niet verdiend heb: hoeveel reken ik? En die vraag komt na een veel goedkopere: heeft iemand hier iets aan?

Zolang ik het tweede niet weet, is de webhook code die ik onderhoud en niet gebruik. De dag dat het wél nodig is zet ik hem in één middag terug, en dan met een besloten prijsmodel in plaats van twee geërfde voorbeeldplannen.

Wat er nog meer sneuvelt in die eerste snoeibeurt

Stripe reist niet alleen. In een MVP van drie dagen voor incidentenbeheer heet commit nummer twee van de repo letterlijk "remove useless things". Twee uur en elf minuten na de eerste. Weg gingen het complete adminpaneel (client-page.tsx met 250 regels, de gebruikerstabel met 380, de grafiek met 114), de feedbackroutes, de feedbacktabel en het bijbehorende schema. De Stripe-routes werden niet verwijderd: ze werden hernoemd naar .txt. route.ts naar route.txt. Ze compileren niet meer, ze bestaan niet meer voor Next, en ze staan er nog als ze ooit nodig zijn.

De dag erna sneuvelde in datzelfde project de magic link. De authpagina had twee routes: Google en e-mail met verificatiecode, met zijn nonce, zijn magicLinkSent, zijn magicLinkCode, zijn handleMagicLinkCode. De commit heet "eliminar magic link (solo Google)" en haalt 116 regels van de pagina weg. In een interne app voor een bedrijf waar iedereen een Google-account heeft, voegde de tweede route alleen maar states toe om te onderhouden.

In een andere template, een paar maanden geleden, kreeg de admin dezelfde snoeibeurt: weg /admin, weg /account, weg hasAdminRole(), weg ADMIN_EMAILS uit de .env.example. En weg vier kolommen uit het gebruikersschema die alleen het paneel dienden: role, banned, ban_reason, ban_expires. Plus impersonated_by in de sessietabel.

Die laatste vind ik het mooiste voorbeeld. Een veld impersonated_by in sessions is geen gemakje: het is een beveiligingsbeslissing die iemand moet begrijpen voordat hij aan de authenticatie komt. Dat "voor het geval dat" meeslepen in een project zonder adminpaneel is een deur met slot achterlaten, zonder muur eromheen.

Waar ik het wél liet staan

Het is geen regel tegen betalingen. Het is een regel over wanneer.

In een SaaS voor beeldgeneratie kwam Stripe er vroeg in en bleef: checkout aan, en zes dagen later de webhook actief en de dagelijkse credits werkend. Daar was afrekenen geen bedrijfshypothese, maar kostenbeheersing: elke generatie kost echt geld op het moment dat iemand op de knop drukt. Zonder credits is er geen product, maar een rekening.

Hetzelfde in een transcriptie-API: de billing en de limieten van het gratis plan werden geschreven toen het gebruikersaccount nog in aanbouw was, omdat een verwerkte minuut audio vanaf het eerste verzoek een prijs heeft.

Naast elkaar gezet is het oordeel duidelijk: kost elk gebruik je geld, dan is afrekenen infrastructuur en gaat het mee vanaf dag één. Zijn de marginale kosten nul, dan is afrekenen product en komt het als je weet wat je verkoopt. Een urenregistratietool, een interne incidentenbeheerder of een template kosten niets per gebruiker. Een beeldmodel wel.

Wat er overblijft na het snoeien

Na dat eerste uur is wat in alle projecten overleeft altijd hetzelfde: auth met één enkele provider, de database met de domeintabellen en niets meer, en de routes die de app nodig heeft om zijn ding te doen. Dat noem ik infrastructuur. Al het andere is een weddenschap op de toekomst van het product, en weddenschappen betaal je in onderhoud, ook als je niet wint.

Een complete template bespaart je de eerste middag. Weghalen wat niet aan de beurt is bespaart je de volgende drie maanden.