Sklep, w którym towar jest kodem
pluginfinance.com to serwis prezentujący i dystrybuujący wtyczki dla branży finansowej. Wdrożenie ruszyło w 2012 roku, obejmowało także identyfikację wizualną, a pierwsza wersja powstała w około sześć tygodni. Platforma była później utrzymywana i modernizowana, dlatego dziś stoi na PHP 8, bazie MySQL 8, WordPressie jako warstwie treści i froncie napisanym w React, który rozmawia z zapleczem przez REST i GraphQL.
Punktem wyjścia nie był wygląd, tylko charakter towaru. Sprzedaż oprogramowania różni się od sprzedaży rzeczy fizycznej w trzech miejscach naraz i każde z nich dotyka innej części systemu. Po pierwsze, towar nie kończy się w magazynie, więc pojęcie stanu zapasu nie istnieje, za to istnieje pojęcie wersji. Po drugie, zakup nie kończy transakcji, bo kupujący oczekuje aktualizacji przez cały okres licencji, czyli sklep musi mieć trwałą relację z instalacją klienta. Po trzecie, produkt techniczny kupuje się po parametrach, a nie po zdjęciu, więc karta produktu jest właściwie dokumentacją.
W tym miejscu w opisach realizacji zwykle stoi zestaw liczb. Tutaj nie ma żadnej, i jest to decyzja świadoma: dane sprzedażowe należą do klienta, a nie do naszego portfolio. To, co da się pokazać rzetelnie, to podjęte decyzje i ich konsekwencje.
Model treści dla produktu, który ma specyfikację
Karta wtyczki finansowej odpowiada na inne pytania niż karta zwykłego produktu. Kupujący chce wiedzieć, z jaką wersją WordPressa i PHP produkt współpracuje, jakie zależności zewnętrzne wnosi, czy działa w środowisku wielojęzycznym, co dokładnie robi z danymi płatniczymi i jak wygląda ścieżka wsparcia po zakupie. Żadnej z tych informacji nie da się sensownie trzymać w opisie tekstowym, bo wszystkie muszą być filtrowalne i porównywalne.
Dlatego produkty opisują dedykowane typy treści i pola niestandardowe, oparte o Advanced Custom Fields. Parametr techniczny jest osobnym polem, a nie zdaniem w akapicie. Ma to dwie konsekwencje, z których druga bywa zaskoczeniem. Pierwsza jest oczywista: po polu można filtrować i sortować. Druga dotyczy utrzymania: kiedy WordPress wydaje nową wersję główną, aktualizacja informacji o zgodności dla całego katalogu jest jedną operacją na polu, a nie przeglądaniem kilkudziesięciu opisów w poszukiwaniu zdania o wersji. Katalog, w którym zgodność wpisana jest w prozę, po dwóch latach kłamie w połowie pozycji i nikt tego nie zauważa.
Osobnym typem treści są recenzje, studia przypadku i materiały eksperckie. Rozdzielenie ich od kart produktowych nie jest porządkiem dla samego porządku. Materiał redakcyjny o tym, jak zintegrować bramkę płatniczą, ma inny cykl życia niż karta produktu: jest czytany przez ludzi, którzy jeszcze niczego nie kupili, żyje latami i to on przyciąga ruch z wyszukiwarki. Karta produktu żyje tak długo, jak produkt. Trzymanie obu w jednym typie treści kończy się tym, że archiwum sklepu miesza artykuły z towarem, a filtrowanie przestaje znaczyć cokolwiek.
Trzecim typem treści jest dokumentacja techniczna produktu, oddzielona zarówno od karty sprzedażowej, jak i od bloga. Dokumentacja ma wersje. Instrukcja instalacji dla wydania sprzed roku nie może zniknąć w chwili, w której ukazuje się nowe, bo część klientów świadomie zostaje na starszej gałęzi. Trzymanie dokumentacji jako podstron karty produktu wygląda porządnie do pierwszej większej zmiany API, po której trzeba pokazać dwie wersje tej samej instrukcji jednocześnie. Osobny typ treści z polem wersji rozwiązuje to bez przebudowy.
Licencja jako trwała relacja, nie jako jednorazowa transakcja
Warstwę handlową obsługuje WooCommerce, rozszerzony o subskrypcje i licencje. Tutaj leży najtrudniejsza część projektu, bo w momencie zakupu sklep zaczyna, a nie kończy pracę.
Klucz licencyjny wygenerowany po zapłacie jest tylko początkiem. Instalacja klienta odpytuje serwis o dostępność aktualizacji, więc sklep pełni jednocześnie funkcję serwera aktualizacji. To znaczy, że żądania przychodzą nie tylko od ludzi z przeglądarką, ale też od setek instalacji WordPressa działających w tle, według własnego harmonogramu, niezależnie od ruchu na stronie. Ten ruch jest niewidoczny w statystykach odwiedzin i bardzo widoczny w obciążeniu serwera, jeśli nikt go nie przewidział.
Rozwiązanie polega na oddzieleniu tych dwóch światów. Punkt końcowy sprawdzający licencję i wersję odpowiada możliwie krótko, korzysta z pamięci Redis i nie uruchamia pełnego cyklu ładowania strony. Sam plik aktualizacji jest oddawany z warstwy brzegowej, bo to statyczny plik, a nie wynik zapytania do bazy. Rozdzielenie kontroli uprawnień od wydania pliku jest tu kluczowe: sprawdzenie licencji jest tanie i musi być dokładne, natomiast transfer pliku jest drogi i powinien odbywać się jak najdalej od aplikacji.
Druga trudność dotyczy wygaśnięcia licencji. Wygasła licencja nie może wyłączyć zainstalowanej wtyczki, bo klient zapłacił za oprogramowanie, które ma nadal działać. Wygaśnięcie odcina prawo do aktualizacji i do wsparcia, a nie prawo do używania. Ten rozdział wygląda na niuans prawny, a jest wymaganiem wprost przekładającym się na kod: stan licencji i stan działania wtyczki muszą być dwiema niezależnymi wartościami, inaczej pierwsze odnowienie po terminie zostawia klientowi wyłączony moduł na produkcji i awarię, za którą odpowiada sklep.
Trzecia trudność jest natury podatkowej i dotyczy każdego sklepu z towarem cyfrowym sprzedawanym poza granicę. Usługa elektroniczna świadczona konsumentowi w innym kraju Unii jest opodatkowana według stawki kraju nabywcy, a sprzedaż przedsiębiorcy z ważnym numerem VAT UE rozlicza się inaczej niż sprzedaż osobie prywatnej. Sklep musi więc rozpoznać status kupującego przed pokazaniem ceny końcowej, zweryfikować numer VAT w rejestrze i zachować dowody lokalizacji nabywcy. To wymaganie wchodzi głęboko w koszyk i w szablon faktury, a nie jest sprawą księgowości po fakcie, bo cena widoczna na stronie zależy od odpowiedzi na te pytania.
Front w React i powód, dla którego nie zawsze warto
Interfejs został zbudowany jako aplikacja po stronie przeglądarki, z WordPressem w roli zaplecza treści i API. To rozwiązanie, które w tym projekcie miało konkretne uzasadnienie i którego nie polecamy odruchowo każdemu sklepowi.
Uzasadnienie brzmi: katalog techniczny jest filtrowany w wielu wymiarach naraz. Kupujący zawęża listę po typie integracji, po zgodności z wersją, po modelu licencji i po kilku innych atrybutach, zmieniając zdanie po drodze. W klasycznym modelu każda zmiana filtra to przeładowanie strony i pełny cykl generowania widoku po stronie serwera. Przy złożonym katalogu oznacza to kilkanaście zapytań do bazy przy każdym kliknięciu w pole wyboru. Front oddzielony od zaplecza pobiera tu wyłącznie listę wyników, a nie cały widok.
Koszt tego wyboru jest realny i warto go nazwać. Treść generowana w przeglądarce bywa gorzej widoczna dla robotów, więc strony, które mają przynosić ruch z wyszukiwarki, czyli karty produktów i materiały redakcyjne, są renderowane po stronie serwera i podawane w gotowym HTML. Tylko sama obsługa katalogu jest interaktywna. Drugi koszt to warstwa API, która staje się kolejną granicą do utrzymania i zabezpieczenia. GraphQL rozwiązuje część problemu, bo pozwala pobrać jednym zapytaniem dokładnie te pola, które widok potrzebuje, zamiast trzech żądań REST zwracających nadmiar danych, ale sam wymaga uważnego ograniczania złożoności zapytań. Publiczny punkt GraphQL bez limitu zagnieżdżenia jest zaproszeniem do wyczerpania zasobów serwera jednym żądaniem.
Warto dodać obserwację o samym katalogu, bo powtarza się w każdym sklepie z oprogramowaniem. Kupujący narzędzie finansowe porównuje zwykle dwie lub trzy pozycje i potrzebuje ich zestawienia obok siebie, atrybut przy atrybucie. Tabela porównawcza jest jednak tak dobra, jak kompletność pól: jeśli połowa produktów ma puste pole zgodności, zestawienie wprowadza w błąd skuteczniej, niż pomaga. Dlatego pola kluczowe dla porównania są wymagane przy publikacji produktu, a nie opcjonalne. To decyzja redakcyjna wymuszona technicznie i jedyna, która utrzymuje sens porównywarki po dwóch latach dokładania produktów.
Cache przy sklepie, czyli gdzie kończy się warstwa brzegowa
Redis obsługuje pamięć obiektową i sesje, Cloudflare stoi jako warstwa brzegowa, a do tego dochodzi cache stron po stronie serwera. Trzy poziomy brzmią jak nadmiar dopóty, dopóki nie rozpisze się, który rodzaj żądania trafia gdzie.
Sklep dzieli ruch na dwie nierówne części. Strony katalogu i materiały redakcyjne są identyczne dla wszystkich odwiedzających, więc mogą być oddawane z warstwy brzegowej i nigdy nie dotykać PHP. Koszyk, konto klienta, historia zamówień i lista kluczy licencyjnych są indywidualne i nie mogą być cache’owane nigdzie poza przeglądarką ich właściciela. Granica między tymi dwoma światami jest najczęstszym źródłem poważnych błędów w sklepach: jeden źle skonfigurowany nagłówek i warstwa brzegowa zaczyna oddawać stronę koszyka jednego klienta drugiemu.
Dlatego reguła jest twarda i jednoznaczna: obecność ciasteczka sesji zakupowej wyklucza żądanie z cache brzegowego, bez wyjątków i bez prób optymalizowania tej zasady. Stanowi to koszt wydajnościowy dla zalogowanych klientów i jest to koszt świadomie zaakceptowany, bo alternatywą jest wyciek danych zamówienia. Fragmenty zależne od sesji, takie jak licznik koszyka w nagłówku, są dociągane osobnym żądaniem po załadowaniu strony, dzięki czemu sam szkielet strony pozostaje wspólny dla wszystkich i nadal może być cache’owany.
Redis skraca to, co pozostaje po stronie PHP. W sklepie z rozbudowanymi atrybutami produktów największy koszt generują zapytania po metadanych, bo baza musi łączyć tabelę produktów z tabelą właściwości przy każdym filtrowaniu. Wyniki tych zapytań, wraz z listami atrybutów i strukturą kategorii, leżą w pamięci obiektowej i są unieważniane przy zmianie produktu, a nie po upływie czasu.
Bezpieczeństwo produktu okołofinansowego
Serwis sprzedający narzędzia dla branży finansowej jest atrakcyjnym celem z dwóch powodów jednocześnie: przetwarza płatności i dystrybuuje kod, który klienci instalują na własnych stronach. Druga część jest poważniejsza, bo skompromitowany plik aktualizacji rozchodzi się automatycznie na wszystkie instalacje.
Z tego wynika kolejność zabezpieczeń. Płatności nie przechodzą przez serwis, tylko przez operatora płatności, więc dane karty nigdy nie trafiają na nasz serwer, a sklep przechowuje wyłącznie identyfikator transakcji. Pliki wydań są podpisane i wydawane wyłącznie po weryfikacji licencji, a dostęp do panelu wydawniczego jest odseparowany od zwykłej administracji treścią. Zabezpieczenia realizujemy w kodzie i w konfiguracji serwera, a nie kolejną wtyczką ochronną. Wtyczka takiego typu działa wewnątrz PHP, czyli już po uruchomieniu aplikacji, obciąża każde żądanie i sama regularnie bywa źródłem podatności. Filtrowanie na wejściu, kontrola uprawnień przy każdej operacji na licencji i twarda konfiguracja serwera kosztują mniej i działają wcześniej.
Do tego dochodzi warstwa danych osobowych, której w sklepie z kluczami licencyjnymi nie da się ominąć. Konto klienta wiąże adres e-mail z listą domen, na których działają jego licencje, czyli z informacją o infrastrukturze jego firmy. To nie są dane wrażliwe w rozumieniu przepisów, ale są danymi, których wyciek ma dla klienta realne konsekwencje, bo pokazuje atakującemu, jakie narzędzia stoją na jakiej stronie. Dostęp do tej listy jest więc ograniczony po stronie API tak samo jak dostęp do danych zamówienia, a logi celowo nie zapisują pełnych kluczy licencyjnych, tylko ich skrócone identyfikatory.
Wydajność, monitoring i utrzymanie
Warstwa frontowa jest budowana z kompilacją zasobów, dzieleniem paczek na mniejsze części i ładowaniem kodu dopiero tam, gdzie jest używany. Obrazy są przetwarzane przy wgraniu, a nie w locie przy każdym wyświetleniu. Statyki idą przez Cloudflare, co przy kliencie międzynarodowym jest największą pojedynczą oszczędnością.
Monitoring w sklepie z licencjami ma jeden priorytet ponad pozostałymi: czas odpowiedzi punktu sprawdzającego licencję. Strona główna, która ładuje się wolno, kosztuje konwersję. Punkt licencyjny, który przestaje odpowiadać, powoduje, że setki instalacji klientów zgłaszają błąd aktualizacji w tym samym momencie, a dział wsparcia dostaje falę zgłoszeń o awarii, która nie wydarzyła się u klienta. Dlatego ten punkt ma osobny alarm, niezależny od ogólnego monitoringu dostępności strony.
Utrzymanie obejmuje aktualizacje silnika, motywu i rozszerzeń, przegląd logów, kopie zapasowe z ćwiczonym odtworzeniem oraz zmiany funkcjonalne wynikające z rozwoju oferty. W sklepie z oprogramowaniem dochodzi jeszcze jedna rzecz, której nie ma w zwykłym e-commerce: każda aktualizacja WordPressa po stronie sklepu musi być zestawiona z deklaracją zgodności sprzedawanych produktów. Sklep, który sam działa na najnowszej wersji, a sprzedaje wtyczki oznaczone jako zgodne z wersją sprzed dwóch lat, podważa własny katalog.
Testy przed każdym wdrożeniem idą na kopii produkcji, nie na pustej instalacji. Powód jest tu bardziej dosłowny niż w innych projektach: pusta instalacja nie ma ani jednej aktywnej licencji, więc cała logika odnowień, wygaśnięć i sprawdzania wersji nie ma na czym się uruchomić. Dopiero rzeczywisty zbiór licencji, z częścią wygasłych i częścią odnowionych w trakcie okresu, pokazuje, czy reguły zachowują się tak, jak opisuje je regulamin sprzedaży.
Podsumowanie
Projekt pokazuje, że WordPress da się wykorzystać do dystrybucji oprogramowania, o ile potraktuje się go jako warstwę treści i handlu, a nie jako całą aplikację. Strukturalny opis produktów, rozdzielenie prawa do używania od prawa do aktualizacji, jasna granica między tym, co wolno cache’ować, a tym, czego nie, oraz oddzielenie sprawdzania licencji od wydawania plików to cztery decyzje, które przenoszą się na każdy kolejny projekt tego typu.
Nie przenosi się model treści tego katalogu ani reguły licencyjne, bo powstały pod jedną ofertę i jeden regulamin. Kolejne wdrożenie zaczyna się od pytania, co dokładnie klient kupuje i na jak długo, bo odpowiedź na to pytanie przesądza o połowie architektury.
Często zadawane pytania
Najważniejsze odpowiedzi, które pomagają wdrożyć temat w praktyce.
Jakiego zakresu dotyczył projekt pluginfinance.com?
#Jak wyglądał przebieg prac przy pluginfinance.com?
#Co było najtrudniejsze technicznie w pluginfinance.com?
#Co z projektu pluginfinance.com da się wykorzystać przy kolejnym wdrożeniu?
#Potrzebujesz FAQ dopasowanego do branży i rynku? Przygotujemy wersję pod Twoje cele biznesowe.
Porozmawiajmy