To są tematy, w których frontend i backend bardzo mocno na siebie wpływają. Wybór między SSE a WebSocketami zmienia sposób budowania . Źle obsłużony webhook potrafi podwoić zamówienie. Integracja bez retry i limitów zaczyna losowo psuć checkout. Niejasny model uwierzytelniania kończy się losowymi 401, problemami z cookie i debugowaniem, które wygląda jak szukanie błędu po omacku.
1. Real-time — WebSocket, SSE, polling
REST dobrze obsługuje klasyczny model „zapytaj i dostań odpowiedź”. Nie wystarcza jednak wtedy, gdy serwer ma sam poinformować użytkownika o zmianie: nowej wiadomości, statusie zadania, postępie generowania odpowiedzi AI albo aktualizacji dashboardu. Wtedy wchodzą , SSE i WebSockets.
Polling — najprostsze rozwiązanie
Polling jest najprostszy mentalnie: frontend pyta co kilka sekund, czy coś się zmieniło. Nie wymaga specjalnej infrastruktury, ale płacisz za to opóźnieniem i dodatkowymi requestami.
Long polling — request wisi otwarty, dopóki backend nie ma czego zwrócić:
Long polling działa prawie wszędzie i bywa dobrym fallbackiem, ale w nowych projektach zwykle warto najpierw rozważyć SSE albo WebSockets.
Server-Sent Events (SSE)
SSE to jednokierunkowy strumień serwer → klient. Działa na zwykłym HTTP i ma automatyczny reconnect, więc świetnie pasuje do sytuacji, w których klient nie musi odsyłać wiadomości tym samym kanałem.
Idealne dla: powiadomień, feedów, AI streaming (ChatGPT-style), live progress.
Na co uważać: przy HTTP/1.x przeglądarki mają niski limit równoległych połączeń do jednej domeny, więc wiele otwartych kart z SSE może zablokować kolejne strumienie. Przy HTTP/2 limit jest negocjowany jako liczba strumieni, ale nadal zależy od konfiguracji klienta, serwera i proxy. Drugi praktyczny detal: natywny EventSource nie pozwala wygodnie dodać nagłówka Authorization, więc uwierzytelnienie zwykle rozwiązuje się przez cookie, podpisany URL albo krótkotrwały token w query stringu.
WebSockets
WebSocket tworzy dwukierunkowy, trwały kanał. To już nie jest zwykły request-response, tylko osobny protokół (ws:// / wss://) z handshake'iem opartym na HTTP Upgrade.
Idealne dla: chatów, gier, kolaboracji w czasie rzeczywistym (Figma, Notion, Google Docs), live trading.
Wybór
Jeżeli potrzebujesz tylko wysyłać aktualizacje z serwera do klienta, SSE jest prostsze. Jeżeli klient i serwer mają rozmawiać w obie strony w czasie rzeczywistym, wybierz WebSockets.
| Scenariusz | Rozwiązanie |
|---|---|
| Powiadomienia "nowy komentarz" | SSE |
| Chat 1-on-1 / pokoje | WebSockets |
| AI streaming (ChatGPT-like) | SSE |
| Real-time gra | WebSockets |
| Status zadania w tle | SSE / polling |
| Kolaboracja na dokumencie (CRDT) | WebSockets |
Popularne biblioteki i usługi: Socket.IO (abstrakcja nad WS z fallbackami), Pusher (usługa zarządzana), Ably, Soketi (samodzielnie hostowany odpowiednik Pushera), Cloudflare Durable Objects.
Wyzwania real-time
- Skalowanie horyzontalne — drugi serwer nie wie o połączeniach pierwszego. Rozwiązanie: Redis pub/sub, NATS, Kafka albo zarządzany broker jako szyna komunikacyjna.
- Autoryzacja kanałów — samo połączenie nie wystarczy. Backend musi sprawdzić, czy użytkownik ma prawo wejść do pokoju, subskrybować dokument albo dostać event konkretnej organizacji.
- Sticky sessions — load balancer często musi trzymać klienta przy tym samym serwerze albo routing musi być świadomie zaprojektowany.
- Reconnect logic — klient powinien wznowić od ostatniego eventu (
Last-Event-IDw SSE), a backend powinien wiedzieć, czy potrafi odtworzyć pominięte wiadomości. - Heartbeat / ping — połączenie może wisieć jako „otwarte”, mimo że sieć realnie padła. Potrzebujesz heartbeatów i timeoutów po stronie klienta oraz serwera.
- Backpressure — jeśli klient nie nadąża z odbiorem, serwer nie może bez końca buforować wiadomości w pamięci.
- Proxy i — nie każda platforma dobrze wspiera długie połączenia. Zanim wybierzesz WebSockety, sprawdź limity hostingu, -a i load balancera.
2. Webhooks — backend powiadamia inny system
Webhook to odwrócone wywołanie . Zamiast co minutę pytać system płatności „czy faktura jest już opłacona?”, pozwalasz mu samemu poinformować Twoją aplikację, gdy coś się wydarzy. To prosty wzorzec, ale wymaga dyscypliny: podpisy, retry, idempotency i szybka odpowiedź są tutaj krytyczne.
Trzy zasady webhooków
1. Weryfikuj podpis
Dostawca podpisuje payload (najczęściej HMAC-SHA256 z sekretem). Bez weryfikacji ktoś może udawać Stripe i oznaczać zamówienia jako opłacone:
2. Odpowiadaj szybko
Webhook to nie miejsce na ciężką logikę. Dostawca zwykle oczekuje szybkiej odpowiedzi 2xx; timeout albo inny status spowoduje retry. Wzorzec: zweryfikuj podpis, zapisz event, wrzuć pracę do kolejki i odpowiedz 200 OK.
3. Idempotency — przygotuj się na duplikaty
Ten sam webhook może przyjść kilka razy (retry przy timeout). Zapisuj event.id w bazie i ignoruj duplikaty:
Bezpieczny flow webhooka
Najbezpieczniejszy schemat wygląda tak:
- Odbierz
raw body, zanim parser JSON zmieni payload. - Zweryfikuj podpis i świeżość timestampu.
- Zapisz
event.id, typ, payload i status przetwarzania w bazie. - Jeśli
event.idjuż istnieje, odpowiedz200i nie wykonuj logiki drugi raz. - Wrzuć ciężką pracę do kolejki.
- Odpowiedz szybko
2xx. - Worker przetwarza event, aktualizuje status i zapisuje błąd, jeśli coś się nie uda.
To rozdziela dwa problemy: dostarczenie eventu i przetworzenie eventu. Dzięki temu retry dostawcy nie tworzy duplikatów, a awaria Twojej logiki biznesowej nie musi oznaczać utraty informacji o płatności.
Lokalne testowanie
Webhook nie dojdzie bezpośrednio na localhost:3000, bo zewnętrzny system musi mieć publiczny adres, pod który może wysłać request. Do lokalnego developmentu używa się tuneli:
- ngrok —
ngrok http 3000daje publiczny URL - Stripe CLI —
stripe listen --forward-to localhost:3000/webhooks/stripe - Cloudflare Tunnel — alternatywa dla ngrok
- smee.io — proxy używane przez GitHub
Częste źródła webhooków
- Stripe — płatności (
payment_intent.succeeded,invoice.paid) - GitHub —
push,pull_request,issues - Resend / SendGrid — delivery, bounces, complaints
- Slack — slash commands, events
- Twilio — SMS delivery, calls
- Clerk / Auth0 — user lifecycle (signup, deleted)
- Vercel — deployment events
3. Integracje zewnętrzne — gdy Twoja aplikacja woła cudze API
Webhooki są integracją przychodzącą. Druga połowa tematu to integracje wychodzące: Twoja aplikacja woła Stripe, Slacka, OpenAI, system ERP, bramkę SMS albo CRM. Dla frontendu często wygląda to jak jeden przycisk „Wyślij”, ale po stronie backendu trzeba obsłużyć wolne odpowiedzi, limity, retry i częściowe awarie.
Najważniejsza zasada: zewnętrzne API nie jest częścią Twojej aplikacji. Może zwolnić, zwrócić 429, zmienić komunikat błędu, mieć przerwę techniczną albo odpowiedzieć po czasie, gdy użytkownik już zamknął ekran.
Minimalny kontrakt integracji
Każda poważniejsza integracja powinna mieć:
- timeout — request nie może wisieć w nieskończoność,
- retry z exponential backoff — ponawiaj tylko błędy przejściowe, np.
408,429,5xx, - obsługę rate limitów — respektuj
Retry-After, limity konta i limity endpointu, - idempotency key — szczególnie przy płatnościach, zamówieniach i operacjach tworzących zasoby,
- mapowanie błędów — nie pokazuj użytkownikowi surowego błędu dostawcy,
- logowanie request ID dostawcy — ułatwia support i debugowanie,
- kolejkę — gdy operacja jest wolna, droga albo nie musi zakończyć się w tym samym requeście.
Przykład prostego retry z backoffem:
Kiedy kolejka zamiast request-response?
Jeżeli użytkownik musi natychmiast zobaczyć wynik, backend może poczekać na odpowiedź dostawcy. Jeżeli operacja jest ciężka albo podatna na awarie, lepiej zapisać zadanie i przetworzyć je asynchronicznie.
| Scenariusz | Lepszy wzorzec |
|---|---|
| Sprawdzenie kuponu w checkout | request-response |
| Wysłanie e-maila po rejestracji | kolejka |
| Import 20 000 rekordów z CRM | kolejka + status postępu |
| Wystawienie faktury po płatności | webhook + kolejka |
| Synchronizacja katalogu produktów | cron / job cykliczny + retry |
| Powiadomienie UI o zakończeniu joba | SSE, WebSocket albo polling |
Dead-letter queue i widoczność błędów
Retry nie może trwać bez końca. Po kilku nieudanych próbach zadanie powinno trafić do dead-letter queue albo statusu failed, który ktoś może przejrzeć i uruchomić ponownie ręcznie. Bez tego integracja „czasem nie działa”, ale nikt nie wie, które rekordy utknęły.
Dla frontendowca ważny jest też model stanu:
queued— zadanie przyjęte,processing— backend pracuje,succeeded— operacja zakończona,failed_retryable— problem przejściowy, backend spróbuje ponownie,failed_final— potrzebna akcja użytkownika lub supportu.
To pozwala zbudować UI, który nie udaje, że wszystko dzieje się synchronicznie.
4. Autentykacja i autoryzacja
To są dwa różne pojęcia, które często wrzuca się do jednego worka. Autentykacja odpowiada na pytanie „kim jesteś?”, a autoryzacja na pytanie „co możesz zrobić?”. Frontend odczuwa oba mechanizmy przez logowanie, wygasające sesje, ukrywanie elementów UI i błędy 401 / 403, ale prawdziwa decyzja zawsze musi zapaść na backendzie.
| Status | Co oznacza | Reakcja UI |
|---|---|---|
401 | Nie wiadomo, kim jesteś | Spróbuj odświeżyć sesję albo pokaż login |
403 | Wiadomo, kim jesteś, ale nie masz dostępu | Pokaż brak uprawnień, nie przekierowuj na login |
419 / 440 | Sesja wygasła lub CSRF token jest nieważny | Odśwież formularz, poproś o ponowne logowanie |
429 | Za dużo prób logowania lub requestów | Pokaż cooldown i nie spamuj retry |
Autentykacja — "kim jesteś?"
Autentykacja to weryfikacja tożsamości użytkownika.
Metody:
- Hasło — tradycyjne logowanie
- — "Zaloguj przez Google/GitHub"
- Magic link — link logowania na e-mail
- Passkeys — klucze FIDO/WebAuthn, zwykle odblokowywane biometrią, PIN-em albo kluczem sprzętowym
Autoryzacja — "co możesz robić?"
Autoryzacja to sprawdzanie uprawnień. UI może ukryć przycisk „Usuń użytkownika”, ale backend i tak musi sprawdzić rolę przy samym requeście.
JWT (JSON Web Token)
JWT to format tokenu, a nie magiczny zamiennik sesji. Często sprawdza się w API, aplikacjach mobilnych i integracjach między usługami, ale w klasycznych aplikacjach webowych sesje nadal bywają prostsze i bezpieczniejsze operacyjnie.
Flow:
- Użytkownik loguje się → serwer tworzy krótko żyjący token
- Frontend trzyma go bezpiecznie (np. w pamięci) albo korzysta z mechanizmu ciasteczek (cookies)
- Przeglądarka wysyła token w nagłówku lub cookie
- Serwer weryfikuje podpis albo identyfikator sesji przy każdym request
Sessions
Alternatywa dla JWT — stan przechowywany na serwerze:
JWT vs sesje — kompromisy
| Cecha | Sesja | JWT |
|---|---|---|
| Stan | Na serwerze (Redis/DB) | W tokenie (stateless) |
| Wylogowanie | Natychmiast (delete sesji) | Trudne — token żyje do exp |
| Skalowanie | Wymaga wspólnego magazynu sesji | Dobrze skaluje się bezstanowo |
| Rozmiar requestu | Małe ID | Cały token w każdym requeście |
| Mobile / native | OK | Często wybierane (brak cookies) |
| Cross-domain | Trudne | Łatwe |
Reguła kciuka: monolit / web → sesje, API mikrousług / mobile → JWT (z krótkim TTL i refresh rotation).
W klasycznej aplikacji webowej sesja w HttpOnly cookie jest często prostsza operacyjnie niż własny system JWT. JWT ma sens, ale wymaga świadomego planu na krótki czas życia, rotację, unieważnianie, wylogowanie ze wszystkich urządzeń i wyciek tokena.
Cookies — szczegóły, które mają znaczenie
Cookies wyglądają niepozornie, ale przy autentykacji każdy flag ma znaczenie. Jedna błędna konfiguracja potrafi zrobić różnicę między rozsądnym modelem bezpieczeństwa a tokenem, który wycieka przez XSS albo jest wysyłany w niepożądanym kontekście.
| Flag | Co robi | Po co |
|---|---|---|
| HttpOnly | JS nie ma dostępu (document.cookie) | Ochrona przed kradzieżą tokena przez XSS |
| Secure | Wysyłane tylko po HTTPS | Brak transmisji w postaci jawnej |
| SameSite=Strict | Cookie tylko z requestów z tego samego site | Pełna ochrona CSRF, ale ubija UX (np. kliknięcie z e-maila wymaga ponownego loginu) |
| SameSite=Lax (domyślne) | Cookie idzie z top-level GETami z innych site, ale nie z formularzami POST | Dobry default |
| SameSite=None | Cookie idzie zawsze (wymaga Secure) | Embedy iframe, third-party |
| Path | Tylko dla podanego path | Ograniczenie zasięgu |
| Domain | Subdomeny mogą czytać | .example.com = wszystkie subdomeny |
| Max-Age / Expires | TTL | Po wygaśnięciu cookie znika |
CSRF — kuzyn XSS
Jeśli używasz cookies do uwierzytelniania, musisz rozumieć CSRF (Cross-Site Request Forgery). Atakujący nie musi znać tokena użytkownika. Wystarczy, że użytkownik jest zalogowany, a przeglądarka automatycznie dołączy cookie do requestu wysłanego z obcej strony.
Przeglądarka sama dołączy cookie uwierzytelniające do tego POST-a, jeśli SameSite na to pozwala.
Obrona (warstwy):
SameSite=LaxlubStrictna cookie uwierzytelniającym — chroni większość typowych scenariuszy automatycznie.- CSRF token (double-submit cookie) — backend generuje token, frontend odczytuje go i dokleja w headerze do requestów zmieniających stan. Atakująca strona nie może odczytać Twojego cookie (Same-Origin Policy).
- Sprawdzaj
Origin/Referer— backend odrzuca requesty zmieniające stan, jeśliOriginnie pasuje do allowlisty.
JWT w localStorage jest odporny na CSRF (przeglądarka nie wysyła go automatycznie), ale podatny na XSS — kompromis idzie w drugą stronę. JWT albo sesja w HttpOnly cookie ogranicza kradzież tokena przez JavaScript, ale wymaga ochrony przed CSRF przez SameSite, token CSRF i kontrolę Origin.
Nie ma jednego „najlepszego” miejsca na token dla każdej aplikacji. Dla klasycznego weba najczęściej wygrywa cookie HttpOnly + Secure + SameSite=Lax/Strict. Dla korzystającej z OAuth coraz częściej spotkasz wariant: krótko żyjący access token w pamięci aplikacji i refresh token w bezpiecznym, HttpOnly cookie albo obsługę uwierzytelniania przez BFF.
Refresh token rotation
Krótki access token (15 min) + długi refresh token (dni / tygodnie). Frontend odświeża access w tle, zanim wygaśnie.
Rotation detection — jeśli ktoś użyje starego, już rotowanego refresh tokena, system zakłada kradzież i unieważnia całą rodzinę tokenów (wszystkie sesje użytkownika). Legalny użytkownik zostanie wylogowany — ale bezpiecznie.
Z perspektywy frontendu są tu trzy ważne reguły:
- Nie rób nieskończonej pętli refresh. Jeśli refresh zwróci
401, wyczyść stan i pokaż login. - Zablokuj równoległe refreshe. Gdy pięć requestów naraz dostanie
401, tylko jeden powinien odświeżać token, a reszta poczekać na wynik. - Rozróżniaj brak uwierzytelnienia od braku uprawnień.
401może uruchomić refresh, ale403powinien pokazać brak dostępu.
MFA — drugi czynnik
Hasło to wiedza ("co znasz"). MFA dodaje "co masz" lub "kim jesteś":
- TOTP — Google Authenticator / Authy (RFC 6238, kod 30s)
- WebAuthn / Passkeys — klucze sprzętowe (YubiKey), klucze platformowe albo biometria (Touch ID, Face ID)
- SMS — wygodne, ale podatne na SIM swap
- Push notifications — Microsoft Authenticator, Duo
Passkeys są oparte o kryptografię klucza publicznego: serwer przechowuje klucz publiczny, a klucz prywatny zostaje na urządzeniu albo w zaufanym menedżerze poświadczeń. Dzięki temu serwer nie zna sekretu, który da się wyłudzić phishingiem tak łatwo jak hasło. W praktyce nadal trzeba jednak przewidzieć recovery, zmianę urządzenia i użytkowników bez kompatybilnego środowiska.
Gotowe rozwiązania
Auth jest jednym z tych obszarów, gdzie „napiszę sam, będzie szybciej” bardzo rzadko kończy się dobrze. W produkcji korzystaj ze sprawdzonych narzędzi albo przynajmniej z dojrzałych bibliotek.
- Auth.js / NextAuth — Next.js, otwarte,
- Clerk — usługa zarządzana, świetne , droga przy skali,
- Supabase Auth — w pakiecie z bazą,
- Auth0 / Okta — enterprise,
- Lucia — obecnie bardziej zasób edukacyjny do implementowania sesji niż biblioteka do nowych wdrożeń produkcyjnych,
- Better Auth — nowoczesne, framework-agnostic.
