Uczciwa uwaga o tym dokumencie. To nie jest raport z realnego zlecenia dla klienta — nie mam w portfolio due diligence, którego treść wolno mi pokazać. To raport wzorcowy: napisany po to, żeby pokazać strukturę, głębokość analizy i sposób opisywania ustaleń, jakich mogą Państwo ode mnie oczekiwać. Ustalenia dotyczą typowej platformy Next.js + PostgreSQL + Stripe i opisują błędy, które w takich systemach spotyka się najczęściej. Wolę oddać coś prawdziwie napisanego z etykietą „wzór" niż podpisać się pod cudzym raportem albo udawać historię, której nie mam.

Przegląd techniczny platformy „Klient A"

Ocena bezpieczeństwa, utrzymywalności i stanu bieżącego przed etapem skalowania

Zakres: Next.js 14 (App Router) · PostgreSQL 15 · Stripe (Checkout + webhooki) · integracje zewnętrzne
Tryb: wyłącznie do odczytu, bez zmian w kodzie i na produkcji · Nakład: 50 h w dwóch etapach
Odbiorca raportu: właściciel spółki · Wersja zanonimizowana

1. Streszczenie dla właściciela

Czy platforma jest bezpieczna tam, gdzie są pieniądze i dane?
Częściowo. Sama płatność jest poprawnie oddelegowana do Stripe Checkout — dane kart nigdy nie dotykają Państwa serwera i to dobra decyzja architektoniczna. Natomiast potwierdzanie płatności opiera się na powrocie użytkownika do aplikacji, a nie na webhooku (ustalenie K-1). W praktyce oznacza to, że zamówienie może zostać opłacone i nigdy nie zrealizowane, jeśli klient zamknie kartę po zapłacie. Odwrotny scenariusz — realizacja bez płatności — jest przy obecnej konfiguracji mniej prawdopodobny, ale nie wykluczony.

Czy da się ją utrzymać i odtworzyć bez autorów?
Dziś nie w rozsądnym czasie. Kod jest czytelny, ale nie istnieje odtwarzalny opis środowiska (ustalenie W-2): brakuje listy wymaganych zmiennych, opisu zależności zewnętrznych i procedury postawienia systemu od zera. Odtworzenie produkcji przez nowy zespół oszacowałem na 3–5 dni pracy z dostępem do infrastruktury, głównie na odgadywanie konfiguracji. Po wdrożeniu rekomendacji z rozdziału 5 ten czas schodzi do kilku godzin.

Jaki jest stan na dziś?
Platforma jest zdrowa jak na system zbudowany szybko i działający produkcyjnie. Nie znalazłem śladów włamania, wycieku danych ani rażących zaniedbań. Znalazłem 2 ustalenia krytyczne, 3 wysokie, 5 średnich i 6 niskich. Żadne z nich nie wymaga zatrzymania działalności, ale dwa krytyczne powinny zostać naprawione przed zwiększeniem ruchu, bo ich koszt rośnie proporcjonalnie do liczby transakcji.

2. Zestawienie ustaleń

NrUstalenieWagaObszarSzacowany nakład naprawy
K-1Realizacja zamówienia zależna od powrotu użytkownika, webhook Stripe bez obsługi idempotencjikrytycznePłatności2–3 dni
K-2Brak izolacji danych między kontami na poziomie zapytań (autoryzacja tylko w warstwie UI)krytyczneBezpieczeństwo4–6 dni
W-1Klucze API w zmiennych środowiskowych bez rotacji i bez rozdzielenia środowiskwysokieBezpieczeństwo1 dzień
W-2Brak odtwarzalnego opisu środowiska i procedury wdrożeniawysokieUtrzymanie2 dni
W-3Kopie zapasowe bazy wykonywane, ale nigdy nie testowane odtworzeniemwysokieCiągłość1 dzień
S-1…S-5Brak indeksów na kolumnach filtrowanych, zapytania N+1 w panelu, logi bez korelacji, brak monitoringu błędów, migracje bez wersjonowaniaśrednieWydajność, utrzymanie5–7 dni łącznie
N-1…N-6Zależności z zaległymi aktualizacjami, martwy kod, brak testów ścieżki płatności, niespójne nazewnictwo, brak README, TODO w kodzie produkcyjnymniskieJakośćciągłe

3. Ustalenia krytyczne — szczegółowo

K-1. Realizacja zamówienia zależy od powrotu użytkownika do aplikacji

Co znalazłem

Zamówienie zmienia status na opłacone w handlerze strony powrotu (/checkout/success), po odczytaniu identyfikatora sesji Stripe z adresu. Webhook checkout.session.completed istnieje, ale zapisuje wyłącznie log — nie zmienia stanu zamówienia. Handler nie sprawdza też, czy dane zdarzenie było już przetwarzane.

Dlaczego to jest krytyczne

Dwa niezależne scenariusze:

Dowód
// app/checkout/success/page.tsx  (fragment, zanonimizowany)
const session = await stripe.checkout.sessions.retrieve(searchParams.session_id)
if (session.payment_status === 'paid') {
  await db.order.update({ where: { id: session.metadata.orderId },
                          data: { status: 'PAID' } })   // ← jedyne miejsce zmiany statusu
}

// app/api/webhooks/stripe/route.ts  (fragment)
case 'checkout.session.completed':
  logger.info('checkout completed', { id: event.id })   // ← brak zmiany stanu
  break
Rekomendacja
  1. Przenieść zmianę stanu zamówienia do webhooka; strona powrotu ma tylko pokazywać wynik.
  2. Dodać tabelę processed_events z event.id jako kluczem głównym i przetwarzać zdarzenie w transakcji z zapisem tego identyfikatora — powtórka nie zrobi wtedy nic.
  3. Weryfikować podpis stripe-signature (obecnie weryfikowany poprawnie — to zostaje bez zmian).
  4. Dodać zadanie okresowe wychwytujące zamówienia opłacone w Stripe, a niezrealizowane u Państwa — jako siatka bezpieczeństwa.

K-2. Izolacja danych między kontami egzekwowana w interfejsie, nie w zapytaniach

Co znalazłem

Zapytania do bazy w kilku endpointach pobierają rekord po jego identyfikatorze, a przynależność do konta sprawdzana jest dopiero przy renderowaniu widoku. W trzech miejscach (eksport danych, pobranie faktury, szczegóły zamówienia) tej weryfikacji nie ma wcale — wystarczy podmienić identyfikator w adresie.

Dlaczego to jest krytyczne

To najprostsza do wykorzystania klasa podatności: nie wymaga narzędzi, wystarczy zmiana cyfry w URL. Przy danych klientów i fakturach oznacza ryzyko naruszenia ochrony danych osobowych ze wszystkimi tego konsekwencjami, łącznie z obowiązkiem zgłoszenia.

Dowód
// app/api/invoices/[id]/route.ts  (fragment, zanonimizowany)
const invoice = await db.invoice.findUnique({ where: { id: params.id } })
return Response.json(invoice)        // ← brak filtra po organizationId

Sprawdzone wyłącznie przez lekturę kodu; zgodnie z ustaleniami nie wykonywałem żadnych zapytań do środowiska produkcyjnego.

Rekomendacja
  1. Wprowadzić jedną warstwę dostępu do danych, w której każde zapytanie obowiązkowo przyjmuje kontekst konta — tak, by pominięcie filtra było błędem kompilacji, a nie przeoczeniem.
  2. Rozważyć Row Level Security w PostgreSQL jako drugą linię obrony niezależną od kodu aplikacji.
  3. Dopisać testy, które próbują pobrać cudzy zasób i oczekują odpowiedzi 404 — to tanie i wychwytuje regresje.

4. Czego nie sprawdzałem

Rzetelny raport mówi też, gdzie kończy się jego zasięg:

5. Plan działania w kolejności

KiedyCoDlaczego w tej kolejności
Najbliższe 2 tygodnieK-1, K-2Koszt obu rośnie wprost proporcjonalnie do ruchu; naprawa przed skalowaniem jest kilkukrotnie tańsza niż po.
MiesiącW-1, W-2, W-3Zdejmują zależność od autorów systemu i ryzyko utraty danych. W-2 jest warunkiem sensownego onboardingu nowego zespołu.
KwartałS-1…S-5Wydajność i obserwowalność — potrzebne, gdy ruch rośnie, ale nie blokują dziś.
Na bieżącoN-1…N-6Higiena kodu, realizowana przy okazji innych prac.

6. Załączniki