Przejdź do głównej zawartości

Architektura

Trzy warstwy

  1. Frontend (przeglądarka) — cały interfejs, wszystkie obliczenia (zdrowie finansowe, FIRE, projekcje, insighty), powiadomienia in-app, szyfrowanie end-to-end danych oraz tryb lokalny (localStorage). Działa bez serwera.
  2. Managed backend — Supabase — baza (gospodarstwa, subskrypcje), Auth, Row Level Security i funkcje Postgres (logika dostępu wykonywana w bazie).
  3. Własny kod serwerowy — funkcje serverless (route'y /api/* w Next.js) potrzebne tam, gdzie wchodzi sekret lub granica zaufania — przede wszystkim płatności (webhook, checkout) oraz wysyłka e-mail / web push.

Do czego potrzebny jest serwer

  • Webhook płatności — weryfikacja podpisu i zapis statusu subskrypcji kluczem service-role (nie może trafić do przeglądarki).
  • Tworzenie checkoutu — wywołanie API dostawcy tajnym kluczem.
  • Powiadomienia e-mail / web push — wymagają harmonogramu i kluczy.

Czego serwer NIE wymaga

Przechowywania planu, logowania, obliczeń finansowych, insightów, powiadomień in-app ani szyfrowania — to obsługuje frontend + Supabase (RLS). Klucz szyfrujący powstaje na urządzeniu i nigdy nie trafia na serwer.

PWA

Manifest + service worker umożliwiają instalację na telefonie i pracę offline w trybie lokalnym. To także fundament pod przyszłe web push.

Aktualizacje: po nowym wdrożeniu nowy service worker czeka w stanie waiting, a aplikacja pokazuje toast „Nowa wersja — Odśwież". Kliknięcie wysyła do SW SKIP_WAITING, a po controllerchange strona przeładowuje się na nową wersję. Dzięki temu użytkownik nie zostaje na starym cache bez sygnału (a jednocześnie nie przerywamy mu pracy bez pytania).

Trzy zabezpieczenia pilnują, żeby karta otwarta w trakcie wdrożenia nie utknęła na starym kodzie (version skew):

  • Sprawdzanie przy powrocie do karty. Rejestracja SW odpytuje o aktualizację nie tylko przy starcie, ale też przy każdym powrocie fokusu do karty (z przepustnicą), więc toast „Nowa wersja" pojawia się także w sesjach, które przeżyły wdrożenie — również po przywróceniu karty z pamięci (bfcache).
  • Payloady nawigacji RSC zawsze z sieci. Service worker nie cache'uje odpowiedzi routera (nagłówek RSC / parametr _rsc) — stary payload wskazywałby chunki JS poprzedniego wdrożenia i „przypinał" starą wersję.
  • Nagłówek X-App-Build. Serwer zwraca hash aktualnie wdrożonego builda. Gdy import pliku się nie powiedzie, aplikacja porównuje go z buildem wkompilowanym w działającą kartę i — przy rozjeździe — zamiast mylącego błędu prosi wprost o odświeżenie strony.

Bezpieczny margines u góry (notch / pasek stanu). Cały układ używa viewport-fit: cover, więc treść sięga krawędzi ekranu — górny odstęp od paska stanu (zegar, sieć, bateria, notch / Dynamic Island) bierzemy z env(safe-area-inset-top). Na części iPhone'ów WebKit potrafi zwrócić tę wartość jako 0 (i to zarówno w Safari, jak i w PWA z ekranu głównego), przez co nagłówek i pływające przyciski wchodziły pod pasek stanu. Dlatego górny inset ma gwarantowaną dolną granicę: zmienna CSS --safe-area-top = max(env(safe-area-inset-top), 1.5rem). Na zdrowych urządzeniach to zwykłe „nic nie zmieniaj", a gdy env skłamie 0 — układ i tak trzyma bezpieczny odstęp. Używamy jej wszędzie tam, gdzie liczy się górna krawędź (powłoka aplikacji, ekrany pełnoekranowe, przyciski ⚙️ / zaproszenia, panele wysuwane).

Przewijanie na mobile. Jedynym scrollerem jest kontener .app-scroll (position: fixed; inset: 0) — nie okno (body { overflow: hidden }). Dwie konsekwencje:

  • Brak poziomego scrolla. .app-scroll przycina w poziomie przez overflow-x: hidden (nie clip — to drugie iOS < 16 ignoruje i wtedy z overflow-y: auto wraca poziomy pasek).
  • Nowa strona zawsze od góry. Domyślne „scroll to top" Next.js celuje w okno, więc tu nie działa — komponent ScrollToTopOnNav zeruje scroll samego kontenera przy zmianie ścieżki (window.scrollTo był próbowany i pogarszał sprawę). Zmiana tylko parametrów zapytania (/?saved=…) nie przewija.