02Ocena sytuacji5 min

Pierwsze, co wyrzucam z każdego szablonu, to Stripe

Prawie każdy projekt zaczynam od skasowania checkoutu, portalu i webhooka. To nie roztargnienie: to oddzielanie infrastruktury, której zawsze będę potrzebował, od tej, na którą jeszcze nie pora.

Autor: Salva Castells

Zdjęcie okładkowe: MarkusHagenlocher, CC BY-SA 3.0.
W tym wpisie

Zajrzałem do historii aplikacji do rejestracji czasu pracy, którą napisałem rok temu. Pierwszy commit o 15:48. O 16:55, sześćdziesiąt siedem minut później, drugi istotny commit nazywał się "remove Stripe API routes for checkout, portal and webhook". Trzy pliki na zbity pysk: 68 linii checkoutu, 48 portalu, 222 webhooka. Nie napisałem jeszcze nawet głównego ekranu.

To nie był poryw. To jest wzorzec: klonuję boilerplate i pierwsze, co robię, to wyłączam płatności.

Odruch: sklonować i skasować

Boilerplate, którego używam, ma Stripe zamontowany fabrycznie. I dobrze, że ma: kiedy jest potrzebny, gotowa integracja oszczędza dzień. Problem w tym, że przychodzi włączony domyślnie, a włączony znaczy, że coś za sobą ciągnie.

W wewnętrznym szablonie, który odpaliłem tej wiosny, commit "remove Stripe integration and related billing features" był drugim w repozytorium, tego samego dnia co init project. Diff nie ruszał tylko tras. Zabrał ze sobą:

  • Całą tabelę user_billing. customer_id, price_id, sub_credits, otp_credits, initial_sub_date, last_otp_date, has_sub, cancel_next_month. Dziewięć kolumn i indeks na user_id do obsługi płatności, która nie istniała.
  • Zahardkodowane priceId w konfiguracji. Dwa przykładowe plany z prawdziwymi identyfikatorami Stripe (price_1R3Rhw...) skopiowanymi z innego projektu. To bomby zegarowe: nikt ich nie kasuje, ktoś je wdroży.
  • Pięć zmiennych środowiskowych. USE_STRIPE, NEXT_PUBLIC_USE_STRIPE, STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Pięć dodatkowych wpisów w .env.example, nad którymi ktoś będzie musiał się zastanowić, czy są potrzebne.
  • Pierwszą migrację. Skasowana i wygenerowana od nowa, bo tabela billingowa siedziała w środku.

A ConfigFile, deklarujący [key: string]: string | string[] | PricingConfig | PricingConfig[], zmienił się w string | string[]. Typ, który przestał musieć przewidywać plany cenowe, to typ, z którym przez kolejne trzy miesiące pracuje się lepiej.

Dlaczego płatności mówią mi najmniej

Webhook Stripe'a to nie kolejna trasa. To 222 linie, które weryfikują podpis, odróżniają checkout.session.completed od invoice.paid od customer.subscription.deleted i piszą do bazy spoza twojej sesji. To trzeba przetestować, przepuszczając zdarzenia przez CLI, trzeba zrobić idempotentnym i trzeba zdecydować, co się dzieje, gdy to samo zdarzenie przyjdzie dwa razy.

Cała ta praca jest prawdziwa. Tyle że odpowiada na pytanie, na które jeszcze sobie nie zapracowałem: ile biorę? A to pytanie idzie po innym, dużo tańszym w odpowiedzi: czy to się komukolwiek przyda?

Dopóki nie znam odpowiedzi na to drugie, webhook jest kodem, który utrzymuję i którego nie ćwiczę. W dniu, w którym naprawdę będzie potrzebny, wstawię go z powrotem w jedno popołudnie, i tym razem z przemyślanym modelem cenowym zamiast dwóch odziedziczonych przykładowych planów.

Co jeszcze pada przy tej pierwszej przycince

Stripe nie podróżuje sam. W trzydniowym MVP do obsługi zgłoszeń commit numer dwa w repo nazywa się dosłownie "remove useless things". Dwie godziny i jedenaście minut po pierwszym. Zabrał cały panel admina (client-page.tsx z 250 liniami, tabelę użytkowników z 380, wykres ze 114), trasy feedbacku, tabelę feedbacku i jej schemat. Tras Stripe'a nie skasował: zmienił im nazwy na .txt. route.ts na route.txt. Przestają się kompilować, przestają istnieć dla Nexta, i dalej tam są, gdyby kiedyś się przydały.

Następnego dnia, w tym samym projekcie, padł magic link. Strona logowania miała dwie drogi: Google i e-mail z kodem weryfikacyjnym, ze swoim nonce, magicLinkSent, magicLinkCode i handleMagicLinkCode. Commit nazywa się "eliminar magic link (solo Google)" i zabiera 116 linii ze strony. W wewnętrznej aplikacji dla firmy, gdzie wszyscy mają konto Google, druga droga dodawała tylko stany do utrzymywania.

W innym szablonie, kilka miesięcy temu, to samo cięcie dosięgło admina: precz z /admin, precz z /account, precz z hasAdminRole(), precz z ADMIN_EMAILS z .env.example. I precz z czterema kolumnami ze schematu użytkowników, które służyły tylko panelowi: role, banned, ban_reason, ban_expires. Plus impersonated_by w tabeli sesji.

Ta ostatnia najbardziej mi się podoba jako przykład. Pole impersonated_by w sessions to nie wygoda: to decyzja bezpieczeństwa, którą ktoś musi zrozumieć, zanim ruszy uwierzytelnianie. Ciągnięcie jej "na wszelki wypadek" w projekcie bez panelu admina to zostawienie drzwi z zamkiem i bez ściany dookoła.

Gdzie akurat zostawiłem to na miejscu

To nie jest reguła przeciwko płatnościom. To reguła o tym, kiedy.

W SaaS do generowania obrazów Stripe wszedł wcześnie i został: checkout włączony, a sześć dni później działający webhook i dzienne kredyty. Tam płatność nie była hipotezą biznesową, tylko hamowaniem kosztów: każda generacja kosztuje prawdziwe pieniądze w chwili, gdy ktoś naciska przycisk. Bez kredytów nie ma produktu, jest faktura.

Tak samo w API do transkrypcji: billing i limity darmowego planu napisałem jeszcze przy budowaniu konta użytkownika, bo minuta przetworzonego audio ma cenę od pierwszego żądania.

Zestawione obok siebie, kryterium widać jasno: jeśli każde użycie kosztuje cię pieniądze, płatność jest infrastrukturą i idzie od pierwszego dnia. Jeśli koszt krańcowy wynosi zero, płatność jest produktem i idzie wtedy, gdy będziesz wiedział, co sprzedajesz. Narzędzie do rejestracji czasu, wewnętrzny system zgłoszeń czy szablon nie zużywają nic na użytkownika. Model generujący obrazy — owszem.

Co zostaje po przycince

Po tej pierwszej godzinie we wszystkich projektach przeżywa zawsze to samo: auth z jednym dostawcą, baza danych z tabelami domeny i niczym więcej, oraz trasy, których aplikacja potrzebuje, żeby robić swoje. To nazywam infrastrukturą. Cała reszta to zakład o przyszłość produktu, a zakłady płaci się utrzymaniem, nawet gdy nie wygrasz.

Kompletny szablon oszczędza ci pierwsze popołudnie. Skasowanie tego, na co nie pora, oszczędza ci kolejne trzy miesiące.