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.
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.