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
Zobacz stronę live ↗
whywepray - własny sklep na Next.js 15 zamiast Shopify

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ą.
Hero SS25 z marquee informacji o wysyłce i dostępności nad nawigacją. (telefon)

Hero SS25 z marquee informacji o wysyłce i dostępności nad nawigacją.