Ich habe mir die Historie einer Zeiterfassungs-App angeschaut, die ich vor einem Jahr geschrieben habe. Der erste Commit war um 15:48. Um 16:55, siebenundsechzig Minuten später, hieß der zweite relevante Commit "remove Stripe API routes for checkout, portal and webhook". Drei Dateien raus: 68 Zeilen Checkout, 48 Portal, 222 Webhook. Ich hatte noch nicht einmal den Hauptbildschirm geschrieben.
Das war keine Kurzschlussreaktion. Das ist das Muster: Ich klone das Boilerplate, und das Erste, was ich tue, ist, die Zahlungen abzuschalten.
Der Reflex: klonen und löschen
Das Boilerplate, das ich benutze, bringt Stripe ab Werk mit. Und es ist gut, dass es das tut: Wenn man es braucht, spart es einen Tag, es fertig zu haben. Das Problem ist, dass es standardmäßig eingeschaltet kommt, und eingeschaltet heißt, dass es Dinge mitschleppt.
In einem internen Template, das ich diesen Frühling gestartet habe, war der Commit "remove Stripe integration and related billing features" der zweite des Repositorys, am selben Tag wie das init project. Der Diff fasste nicht nur Routen an. Er nahm mit:
- Die komplette Tabelle
user_billing.customer_id,price_id,sub_credits,otp_credits,initial_sub_date,last_otp_date,has_sub,cancel_next_month. Neun Spalten und ein Index aufuser_id, um eine Abrechnung zu verwalten, die es nicht gab. - Die hartkodierten
priceIdin der Config. Zwei Beispielpläne mit echten Stripe-Identifikatoren (price_1R3Rhw...), aus einem anderen Projekt kopiert. Das sind Zeitbomben: Niemand löscht sie, irgendjemand deployt sie. - Fünf Umgebungsvariablen.
USE_STRIPE,NEXT_PUBLIC_USE_STRIPE,STRIPE_PUBLIC_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET. Fünf weitere Einträge in der.env.example, bei denen sich jemand fragen muss, ob sie nötig sind. - Die erste Migration. Sie wurde gelöscht und neu generiert, weil die Billing-Tabelle darin steckte.
Und das ConfigFile, das [key: string]: string | string[] | PricingConfig | PricingConfig[] deklarierte, wurde zu string | string[]. Ein Typ, der keine Preispläne mehr berücksichtigen muss, ist ein Typ, mit dem sich die nächsten drei Monate besser arbeiten lässt.
Warum Zahlungen mir am wenigsten sagen
Ein Stripe-Webhook ist nicht einfach eine weitere Route. Das sind 222 Zeilen, die Signaturen verifizieren, checkout.session.completed von invoice.paid von customer.subscription.deleted unterscheiden und von außerhalb deiner Session in die Datenbank schreiben. Das muss man mit dem CLI testen, indem man Events weiterleitet, man muss es idempotent machen, und man muss entscheiden, was passiert, wenn dasselbe Event zweimal ankommt.
Diese ganze Arbeit ist echt. Nur beantwortet sie eine Frage, die ich mir noch nicht verdient habe: Wie viel verlange ich? Und diese Frage kommt nach einer anderen, die viel billiger zu beantworten ist: Nützt das überhaupt jemandem?
Solange ich das Zweite nicht weiß, ist der Webhook Code, den ich pflege und nicht benutze. An dem Tag, an dem er wirklich gebraucht wird, baue ich ihn an einem Nachmittag wieder ein, und diesmal mit einem entschiedenen Preismodell statt mit zwei geerbten Beispielplänen.
Was bei diesem ersten Rückschnitt noch fällt
Stripe reist nicht allein. In einem Drei-Tage-MVP zur Verwaltung von Störungsmeldungen heißt Commit Nummer zwei des Repos wörtlich "remove useless things". Zwei Stunden und elf Minuten nach dem ersten. Er nahm das komplette Admin-Panel mit (client-page.tsx mit 250 Zeilen, die Nutzertabelle mit 380, das Diagramm mit 114), die Feedback-Routen, die Feedback-Tabelle und ihr Schema. Die Stripe-Routen hat er nicht gelöscht: Er hat sie in .txt umbenannt. route.ts zu route.txt. Sie kompilieren nicht mehr, existieren für Next nicht mehr, und sind trotzdem noch da, falls sie eines Tages gebraucht werden.
Am Tag darauf fiel in demselben Projekt der Magic Link. Die Auth-Seite hatte zwei Wege: Google und E-Mail mit Verifizierungscode, mit ihrem nonce, ihrem magicLinkSent, ihrem magicLinkCode, ihrem handleMagicLinkCode. Der Commit heißt "eliminar magic link (solo Google)" und nimmt 116 Zeilen der Seite mit. In einer internen App für ein Unternehmen, in dem alle ein Google-Konto haben, fügte der zweite Weg nur Zustände hinzu, die man pflegen muss.
In einem anderen Template traf derselbe Schnitt vor ein paar Monaten das Admin: /admin raus, /account raus, hasAdminRole() raus, ADMIN_EMAILS aus der .env.example raus. Und raus mit vier Spalten aus dem Nutzerschema, die nur dem Panel dienten: role, banned, ban_reason, ban_expires. Dazu impersonated_by in der Sessions-Tabelle.
Letzteres gefällt mir am besten als Beispiel. Ein Feld impersonated_by in sessions ist keine Bequemlichkeit: Es ist eine Sicherheitsentscheidung, die jemand verstehen muss, bevor er die Authentifizierung anfasst. Es "für alle Fälle" in einem Projekt ohne Admin-Panel mitzuschleppen heißt, eine Tür mit eingebautem Schloss stehen zu lassen, ohne Wand drumherum.
Wo ich es doch drin gelassen habe
Das ist keine Regel gegen Zahlungen. Es ist eine Regel darüber, wann.
In einem SaaS zur Bildgenerierung kam Stripe früh rein und blieb: Checkout aktiviert, und sechs Tage später der Webhook aktiv und die Tagescredits in Betrieb. Dort war die Abrechnung keine Geschäftshypothese, sondern Kostenkontrolle: Jede Generierung kostet echtes Geld in dem Moment, in dem jemand den Knopf drückt. Ohne Credits gibt es kein Produkt, es gibt eine Rechnung.
Dasselbe bei einer Transkriptions-API: Billing und die Limits des kostenlosen Plans wurden geschrieben, als das Nutzerkonto noch im Aufbau war, denn die verarbeitete Audiominute hat ab der ersten Anfrage einen Preis.
Nebeneinandergestellt wird das Kriterium klar: Wenn jede Nutzung dich Geld kostet, ist die Abrechnung Infrastruktur und gehört ab Tag eins dazu. Wenn die Grenzkosten null sind, ist die Abrechnung Produkt und kommt, wenn du weißt, was du verkaufst. Ein Zeiterfassungswerkzeug, ein internes Störungsmanagement oder ein Template verbrauchen pro Nutzer nichts. Ein Bildmodell schon.
Was nach dem Rückschnitt übrig bleibt
Nach dieser ersten Stunde überlebt in allen Projekten immer dasselbe: Auth mit einem einzigen Provider, die Datenbank mit den Tabellen der Domäne und sonst nichts, und die Routen, die die App braucht, um ihre Sache zu tun. Das nenne ich Infrastruktur. Alles andere ist eine Wette auf die Zukunft des Produkts, und Wetten bezahlt man in Wartung, auch wenn du nicht gewinnst.
Ein vollständiges Template spart dir den ersten Nachmittag. Das zu löschen, was nicht dran ist, spart dir die nächsten drei Monate.
