← Portfolio Case Study

Legacy Rescue — przepisanie porzuconego backendu sprzed 8 lat

Klient: Rodzinna firma usługowa (3 lokalizacje detaliczne, 5 pracowników, ~18 000 zamówień historycznych) Czas: 4 tygodnie
Node.js Express MySQL React 15 JWT bcrypt Caddy PM2

Przed

  • Brak backupów, brak dokumentacji
  • Autoryzacja praktycznie wyłączona przez DEV_MODE=true (wygasła subskrypcja SaaS)
  • 10+ zgłoszonych bugów nienaprawialnych bez ingerencji w kod
  • Poprzedni deweloperzy rezygnowali po kilku dniach analizy kodu

Po

  • 21 punktów kontraktowych dostarczonych w 100%
  • Zero regresji frontendu po pełnej wymianie backendu
  • Własna autoryzacja (bcrypt + JWT) — bez zewnętrznych zależności, bez subskrypcji
  • HTTPS z automatycznie odnawianym certyfikatem Let’s Encrypt
Legacy Rescue — porzucony 8-letni system przepisany do stanu produkcyjnego w 4 tygodnie. 21/21 punktów kontraktowych dostarczonych.

Kontekst

Klient prowadzi 10-letni biznes z trzema punktami detalicznymi w galeriach handlowych. Osiem lat temu zamówił u zewnętrznej agencji autorski system do zarządzania zamówieniami — zastąpił arkusze kalkulacyjne i papierowe segregatory, stając się mission-critical dla codziennej pracy we wszystkich trzech lokalizacjach.

Pierwotny zespół developerski zniknął kilka lat po wdrożeniu. Klient próbował zatrudniać kolejnych programistów do kontynuacji prac — każdy rezygnował po kilku dniach od zajrzenia w kod. Kiedy zgłosił się do mnie, system wciąż działał, ale z narastającymi problemami:

  • brak backupów
  • brak dokumentacji
  • zahardkodowana konfiguracja logowania wskazująca na zewnętrznego dostawcę SaaS (trial niemal na pewno wygasł — autoryzacja była po cichu omijana przez DEV_MODE=true)
  • kilkanaście zgłoszonych bugów, których nie dało się naprawić bez ingerencji w kod
  • koszty hostingu (duży dedykowany VPS) wyraźnie nieproporcjonalne do skali biznesu

Klient przyszedł z dokumentem „plan zmian" zawierającym 21 wymagań funkcjonalnych, z jednym pytaniem: czy ten system warto rozwijać, czy przepisujemy od zera?

Podejście

Zaproponowałem dwuetapowy model współpracy, zamiast wchodzenia w prace na ślepo.

Etap 1 — Płatny audyt techniczny (4 dni)

Zanim cokolwiek tknąłem, przeanalizowałem kod i bazę danych, a następnie dostarczyłem pisemny raport:

  • ocena jakości kodu i bazy danych
  • każde z 21 wymagań zmapowane na konkretne zmiany (które pliki, które tabele, estymacja wysiłku)
  • rekomendacja: rozwijać vs. przepisać z uzasadnieniem
  • wycena pozycyjna etapu wdrożeniowego

Rekomendacja: rozwijać istniejący system. Mimo wieku kod był na tyle czytelny, a dane historyczne na tyle wartościowe, by zachować je bez migracji.

Etap 2 — Umowa z ceną stałą i harmonogramem

Na podstawie audytu zaproponowałem umowę z ceną stałą i pełnym przeniesieniem praw autorskich. Zakres podzielony na 3 grupy:

  • A — Usunięcia (4 punkty): nieużywane moduły i pola usunięte w celu uproszczenia UX
  • B — Naprawy bugów (5 punktów): każde zgłoszone zgłoszenie
  • C — Nowe funkcje (12 punktów): faktyczna wartość biznesowa

Płatność z góry, 5 dni na uwagi klienta po dostarczeniu, kolejne 5 dni na poprawki w ramach pierwotnej ceny.

Wybrane decyzje techniczne

1. Wymiana backendu, zachowanie frontendu

Oryginalny backend (Java Spring Boot + Hibernate ORM) był porzucony. Zamiast go reanimować, przepisałem go na Node.js / Express, zachowując 100% kontraktu API: te same routy, ten sam kształt JSON-a, to samo zachowanie snake_case/camelCase, te same konwersje dat, ta sama semantyka pustych pól. Frontend React 15 nie wymagał zmiany ani jednej linii.

Efekt: zero ryzyka regresji UI, cała złożoność skupiona w jednym miejscu.

2. Generyczny CRUD świadomy schematu

Zamiast pisać po pięć handlerów na każdą tabelę, zaimplementowałem helper crud(router, path, table, options), który:

  • ładuje schemat tabeli przez DESCRIBE przy starcie
  • automatycznie konwertuje typy (tinyint(1) ↔ boolean, DECIMAL ↔ number, DATE ze strefą lokalną)
  • obsługuje mapowanie snake_case ↔ camelCase
  • udostępnia hooki beforeSave dla logiki biznesowej

Dodanie kolejnej tabeli z pełnym CRUD to teraz jedna linia.

3. Migracja bazy danych bez przestoju

Nie zatrzymałem starej aplikacji — postawiłem nowy backend obok niej, wskazałem mu tę samą instancję MySQL i przełączyłem DNS / nginx dopiero po weryfikacji. Stara wersja stała jako siatka bezpieczeństwa rollback przez 72h.

4. Autoryzacja od zera zamiast reanimacji Auth0

Trial płatnego dostawcy dawno wygasł. Zamiast go odnawiać, napisałem własny moduł (bcrypt + JWT + middleware requireRole) na prostej tabeli users. Korzyści dla klienta:

  • zero zewnętrznych zależności
  • zero opłat subskrypcyjnych
  • pełna kontrola nad listą kont

5. HTTPS przez reverse proxy

Caddy 2 + automatyczny Let’s Encrypt + przekierowanie http→https — klient dostaje kłódkę w przeglądarce, nie myśląc nigdy o odnawianiu certyfikatu.

Stack (docelowy)

  • Backend: Node.js 16 + Express + mysql2
  • Autoryzacja: bcrypt + jsonwebtoken + express-rate-limit
  • Frontend: React 15 (react-scripts 0.8) — nieprzepisany
  • Baza danych: MySQL 5.7
  • Process manager: PM2 (autostart systemd po reboocie)
  • Reverse proxy / TLS: Caddy 2 + Let’s Encrypt
  • Hosting: Linux VPS

Wybrane dostarczone funkcje

  • autouzupełnianie klienta po fragmencie telefonu/nazwiska (agregowane z zamówień historycznych)
  • masowe aktualizacje cen z filtrem po serii/numerze, tryby procentowy i kwotowy, podgląd przed zatwierdzeniem
  • masowy wydruk zamówień w jednym wywołaniu (page-break per zamówienie przez CSS print)
  • ilość stanu rozbita z pojedynczej liczby na listę pozycji (np. „3m × 3 + 1m") z automatycznym sumowaniem
  • filtr zamówień zakończonych z masowym oznaczeniem jako zrealizowane i undo
  • kolorystyczne oznaczenie lokalizacji w tabeli
  • role użytkowników (admin / sprzedaż / magazyn) z uprawnieniami writeRoles per tabela
  • 2 szablony wydruków dopasowane do workflow klienta (iterowane na podstawie zdjęć z telefonu wysyłanych SMS-em)

Rezultaty

  • 21 punktów kontraktowych — 100% dostarczone.
  • Zero regresji frontendu po wymianie silnika backendu.
  • Migracja bazy danych bez przestoju (wieczorne okno serwisowe, ~15 minut).
  • HTTPS na własnej subdomenie klienta zamiast adresu IP.
  • Hasła użytkowników zhashowane (bcrypt cost 10), JWT z 30-dniowym TTL.
  • Baza danych backupowana retroaktywnie przed każdym deployem.
  • Tego samego dnia akceptacja klienta, zgoda na kontynuację współpracy.

Proces i miękkie aspekty

Część, którą większość freelancerów pomija:

  • Dostępność po wdrożeniu. Każdy SMS dostawał odpowiedź w godzinę. Każda uwaga — fix w ciągu dnia.
  • Iteracyjne dopracowywanie wydruków na podstawie zdjęć z telefonu od właściciela. Dwie rundy iteracji na szablon wydruku.
  • Decyzje pytane, nie narzucane. Przy każdej niejednoznaczności (struktura ról, format domeny) pytałem i dokumentowałem odpowiedź na piśmie.
  • Transparentna komunikacja przed wdrożeniem. Przed większymi zmianami (autoryzacja, masowe aktualizacje zamówień zakończonych) wysyłałem mailowe uprzedzenie z instrukcją dla personelu na następny poranek.

Wyzwania i lekcje

1. React 15 i React.createClass

Komponenty zbudowane na createClass — bez hooków, bez klas ES6. Ręczne this.bind, mixins, mutacja stanu przez this.state.x = y (antywzorzec, ale spójny z otaczającym kodem). Pokusa modernizacji była silna, ale klient nie zobaczyłby wartości.

2. Kodowanie i MySQL 8 vs 5.7

Lokalny XAMPP (MySQL 5.7) i Docker (MySQL 8) różnie obsługują caching_sha2_password. Fix: skrypty migracyjne uruchamiane przez Node (nie CLI mysql), wszystko trzymane na tym samym kliencie (mysql2).

3. CRLF w plikach .sql

Migracje edytowane na Windowsie miały zakończenia linii CRLF, co okazjonalnie ucinało komendy w MySQL CLI. Fix: uruchamiałem każdą migrację przez Node (split po ;, filtrowanie komentarzy, zapytanie po zapytaniu).

4. bcrypt 6 wymaga Node ≥18, prod ma Node 16

Ostrzeżenie o niewspieranej wersji, ale funkcjonalnie wszystko OK. Zostawione z notatką na kolejną iterację (aktualizacja OS + Node).

5. Propagacja DNS i niejasne nameservery

Domena zarejestrowana u jednego dostawcy, DNS obsługiwane przez innego. Cache publicznych resolverów vs. authoritative DNS — początkowo błędnie zdiagnozowałem problem jako opóźnienie propagacji. Lekcja: zawsze pytaj authoritative nameserver, nie tylko 8.8.8.8.

Dalsze możliwości

Z projektu wyrósł kolejny roadmap: automatyczne powiadomienia SMS do klientów, raporty sprzedażowe, migracja na tańszy hosting (potencjalna oszczędność ~2000 PLN/rok) oraz modernizacja OS + Node. Te elementy są poza zakresem umowy bazowej, dostępne jako osobne etapy.

Co to case study mówi o tym, jak pracuję

  1. Nie wchodzę w legacy w ciemno. Płatny audyt jako osobny etap chroni obie strony przed nierealistycznymi oczekiwaniami.
  2. Rozliczam się za rezultaty, z jasnym zakresem i formalną akceptacją. Klient wie dokładnie, co dostaje, ja wiem dokładnie, co mam dostarczyć.
  3. Myślę o kliencie po wdrożeniu. Zhashowane hasła, backupy przed migracją, dokumentacja stanu produkcji, instrukcje operacyjne — rzeczy, o których nikt nie myśli, dopóki coś się nie zepsuje.
  4. Nie wciskam przepisania „bo nowe jest lepsze". React 15 działa, MySQL 5.7 działa, Node 16 działa. Modernizacja stacku to osobna decyzja biznesowa, nie mój techniczny fetysz.
  5. Transparentna komunikacja, w prostym języku. Klienci nietechniczni dostają maile, które mogą czytać bez mentalnego tłumaczenia z angielskiego.

Zgodnie z § 6 NDA obejmującym kod źródłowy, bazę danych i dane klientów, niniejsze case study pomija: nazwę firmy, dokładną lokalizację, dane osobowe pracowników i klientów, konkretne parametry infrastruktury oraz treść raportu z audytu. Skupia się wyłącznie na procesie, decyzjach technicznych i jakościowych rezultatach.

Masz podobny problem?

Opisz go AI — zbierze kontekst techniczny, Artur przygotuje wycenę w 48h.

Rozpocznij diagnostykę