E-commerce · whywepray · 2025 · 3 miesiące
whywepray - własny sklep na Next.js 15 zamiast Shopify
Sklep, w którym zmiana ceny produktu nie przepisuje historii zamówień, a ponowiony webhook Stripe nie tworzy drugiego zamówienia - dwie rzeczy, które w e-commerce psują się po cichu i wychodzą przy reklamacji.
- Next.js 15
- TypeScript
- Drizzle ORM
- PostgreSQL
- Stripe
- Auth.js
- Redis
- Docker
Wyzwanie
Marka whywepray sprzedaje jeden produkt w kilku rozmiarach, w Europie i do USA, i chciała robić to na własnym sklepie zamiast na Shopify. Powód nie był estetyczny: przy dropach kolekcji i pojedynczym produkcie o wysokiej marży prowizja od obrotu i limity szablonu kosztują więcej niż utrzymanie własnego kodu. Własny sklep oznacza jednak, że to my odpowiadamy za rzeczy, które platforma robi za ciebie po cichu - zaokrąglenia kwot, ponowione webhooki, historię zamówień.
Nasze rozwiązanie
Zbudowaliśmy sklep na Next.js 15 w monorepo, w którym logika domenowa (pieniądze, koszyk, zamówienie, wycena) jest osobnym pakietem bez zależności od bazy i HTTP, a schematy Zod są jedynym źródłem prawdy dla bazy, API i formularzy. Płatności przez hostowany Stripe Checkout ze Stripe Tax, wysyłka darmowa w Europie i €60 flat rate do USA.
103 ms
Czas do pierwszego bajtu
0,40 s
Pierwsze renderowanie treści
814 kB
Transfer strony głównej
Co zbudowaliśmy
- 01 Własny storefront na Next.js 15 - App Router, RSC, Server Actions
- 02 Monorepo pnpm + Turborepo, logika domenowa jako osobny pakiet
- 03 Kwoty jako liczby całkowite w groszach - nigdy float
- 04 Snapshot pozycji zamówienia: cena i nazwa zapisane w chwili zakupu
- 05 Idempotencja webhooków Stripe na Redisie
- 06 Auth.js v5 - hasła argon2id i logowanie Google
- 07 Stripe Checkout ze Stripe Tax, maile transakcyjne przez Resend
- 08 Testy Playwright na ścieżce zakupowej, deploy w Dockerze za Caddy
Spis treści +
Punkt wyjścia
whywepray sprzedaje WHYTE Bootcut: spodnie z japońskiego selvedge denim 13.5 oz, garment-washed. Jeden produkt, kilka rozmiarów, sprzedaż dropami, wysyłka do Europy i USA.
Przy takim profilu Shopify jest kiepskim interesem. Prowizja liczy się od obrotu, a nie od pracy, którą platforma wykonuje - a wykonuje jej niewiele, bo katalog to jedna pozycja. Limity szablonu boli się natomiast przy każdej kampanii, bo marka sprzedaje wyglądem. Rozpisaliśmy ten rachunek szerzej w porównaniu WooCommerce i Shopify na polskim rynku - tu wychodzi analogicznie, tylko po stronie Next.js zamiast WordPressa.
Własny sklep internetowy zdejmuje prowizję i limity, ale przenosi na nas odpowiedzialność za rzeczy, które platforma robi niewidocznie. Te trzy poniżej to miejsca, w których sklep zbudowany naprędce psuje się dopiero przy pierwszej reklamacji.
Decyzje, które ukształtowały projekt
Pieniądze jako liczby całkowite
Kwoty są trzymane w groszach jako integer, nigdy jako liczby zmiennoprzecinkowe, i przechodzą przez jeden typ Money. Powód jest arytmetyczny: 0.1 + 0.2 w zmiennoprzecinkowych nie daje 0.3, a przy koszyku, rabacie, podatku i wysyłce te końcówki się sumują. Klient widzi wtedy inną kwotę niż Stripe, a różnicę wykrywa się przy uzgadnianiu księgowości, miesiąc później.
Snapshot zamówienia zamiast referencji
Pozycja zamówienia zapisuje cenę, nazwę i SKU w chwili zakupu, zamiast wskazywać na aktualny produkt. Dzięki temu podniesienie ceny w kolejnym dropie nie przepisuje wstecz zamówień z poprzedniego, a wycofanie produktu z katalogu nie wybija faktury sprzed pół roku. Sklepy, które trzymają samą referencję, dowiadują się o tym, gdy klient prosi o duplikat potwierdzenia.
Idempotencja webhooków
Stripe gwarantuje dostarczenie webhooka „co najmniej raz”, więc potrafi wysłać ten sam webhook więcej niż jeden raz. Bez zabezpieczenia drugie wywołanie tworzy drugie zamówienie i drugi mail do klienta. Klucze idempotencji siedzą w Redisie, tam też jest ograniczanie liczby żądań na publicznych endpointach.
Logika domenowa poza frameworkiem
Koszyk, wycena i zamówienie żyją w pakiecie core bez importów z bazy, HTTP i Next.js. Testy tej warstwy nie potrzebują serwera ani bazy, a reguła „ile kosztuje wysyłka do USA” jest w jednym miejscu, nie rozsmarowana po komponentach. Warstwa domenowa zwraca Result, wyjątki zostają na granicach systemu.
Zod jako jedno źródło schematów
Schemat bazy, kontrakt API i walidacja formularza wywodzą się z jednej definicji. Bez tego rozjazd jest kwestią czasu: pole staje się opcjonalne w bazie, formularz dalej go wymaga, a błąd widać dopiero w produkcji.
Checkout hostowany, nie własny
Płatności obsługuje hostowany Stripe Checkout ze Stripe Tax. Świadomie nie budujemy własnego formularza karty - dane karty nigdy nie dotykają naszego serwera, a podatek od sprzedaży do dwóch stref liczy Stripe, zamiast nas. To jedyne miejsce w tym projekcie, gdzie oddajemy kontrolę na zewnątrz, i jedyne, gdzie to się opłaca.
Co z tego wyszło
Pomiar strony głównej z 13 września 2026, headless Chrome bez cache: 103 ms do pierwszego bajtu, 404 ms do pierwszego renderowania treści, 989 ms do zdarzenia load. Trzydzieści trzy żądania na całą stronę: jeden dokument, jeden arkusz stylów, osiem obrazów, cztery fonty, szesnaście plików JavaScript i trzy zapytania fetch - i 814 kB transferu, najmniej z naszych ostatnich sklepów. Przy jednym produkcie w katalogu to spodziewane, ale i tak trzeba to nazwać: lekkość tej strony bierze się z małego katalogu, nie tylko z architektury.
Szesnaście plików JavaScript na stronie z jednym produktem to liczba, którą trzeba mieć na oku przy kolejnych dropach - to standardowe rozbicie na chunki w App Routerze, ale przy większym katalogu urośnie, jeśli nikt go nie przypilnuje. To ten sam temat, co w checkliście Core Web Vitals - rezerwę lepiej znać, zanim stanie się problemem. Nie mamy pomiaru sprzed wdrożenia - sklep startował od zera na tym stacku, więc nie ma z czym porównywać, tylko stan po uruchomieniu.
Technicznie
Next.js 15 (App Router, RSC, Server Actions), TypeScript w trybie strict, PostgreSQL z Drizzle ORM, Auth.js v5 z argon2id i Google OAuth, Stripe Checkout i Stripe Tax, Resend z React Email, Redis do idempotencji i limitów. Monorepo na pnpm i Turborepo: apps/web plus pakiety core, contracts, db, ui, observability. Testy w Vitest i Playwright, produkcja w Dockerze za Caddy.
Zrzuty ekranu


Hero SS25 z marquee informacji o wysyłce i dostępności nad nawigacją.
Podobny problem do rozwiązania?
Napisz krótko, o co chodzi, wycena wraca w 24 godziny.
Zapytaj o wycenę →




