Architektura
Trzy warstwy
- 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. - Managed backend — Supabase — baza (gospodarstwa, subskrypcje), Auth, Row Level Security i funkcje Postgres (logika dostępu wykonywana w bazie).
- 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.
Wydajność
- Podział kodu. Ekrany dla osób niezalogowanych (strona startowa, beta, dokumenty prawne), kreator importu oraz słowniki niemiecki i francuski są osobnymi paczkami. Zalogowany użytkownik ich nie pobiera, dopóki z nich nie skorzysta. Strona startowa nadal jest renderowana na serwerze, więc wyszukiwarki widzą jej treść.
- J ęzyk. Polski i angielski są w paczce startowej. Po wybraniu niemieckiego słownik doładowuje się w tle; na tę chwilę interfejs pokazuje angielski.
- Kursy w tle. Gdy karta lub aplikacja jest w tle, odświeżanie kursów (kryptowaluty, ETF, akcje) jest wstrzymane. Po powrocie, jeśli minął pełny okres odświeżania, kursy pobierają się od razu.
- Analiza. Wynik kondycji, zwroty (XIRR, TWR), FIRE i oś czasu liczą się raz na zmianę danych, a nie przy każdym przełączeniu zakładki.
- Pulpit. Wynik zdrowia, trend 12 miesięcy, zdanie „co się zmieniło" i stan rytuału liczą się raz na zmianę danych — tak samo karta głównego celu i panel boczny. Przełączanie zakładek czy otwieranie paneli niczego nie przelicza.
- Portfel. Na komputerze z modułem „Szczegóły inwestycji" montowana jest tylko tabela, na telefonie tylko karty (wcześniej oba, jedno ukryte). Wiersze odświeżają się tylko przy zmianie własnej pozycji. Projekcja każdej pozycji liczy się raz na zmianę danych lub horyzontu.
- Pulpit. Na telefonie montowana jest tylko aktywna zakładka i kompaktowy wykres struktury; na komputerze wszystkie sekcje, wykres z legendą i wykres przychodów — bez ukrytych kopii.
- Wybór wariantu (telefon / komputer) zna szerokość okna już przy pierwszym rysowaniu ekranu, więc na komputerze nie mignie na chwilę wersja mobilna.
- Paczka startowa. Biblioteka Supabase (konto w chmurze), dane trybu demo, słowniki EN/DE/FR i kod Pulpitu doładowują się dopiero, gdy są potrzebne — strona startowa i aplikacja w trybie lokalnym ładują mniej kodu. Prawy panel z podsumowaniami (od 1280 px) na węższych ekranach w ogóle się nie liczy ani nie renderuje.
- Zapis stanu. Zmiany trafiają do pamięci przeglądarki z krótkim opóźnieniem (seria zmian = jeden zapis), a przy zamknięciu lub schowaniu karty od razu. Pola tekstowe planu (nazwa planu, kategorie, inne dochody w FIRE) zapisują się po wyjściu z pola.
- Nawigacja bez rozmycia tła (rozmycie liczyło się przy każdej klatce przewijania); pasek stanu działa tylko na komputerze.
- Znak GrapeVest. Gradienty logo są zdefiniowane raz na stronę, więc logo wygląda tak samo na serwerze i w przeglądarce (bez ponownego renderowania przy starcie strony).
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-scrollprzycina w poziomie przezoverflow-x: hidden(nieclip— to drugie iOS < 16 ignoruje i wtedy zoverflow-y: autowraca 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
ScrollToTopOnNavzeruje scroll samego kontenera przy zmianie ścieżki (window.scrollTobył próbowany i pogarszał sprawę). Zmiana tylko parametrów zapytania (/?saved=…) nie przewija.