Pinezka nie jest jeszcze produktem
Mapa pełna punktów wygląda jak gotowa aplikacja. Do pierwszego kliknięcia. Potem okazuje się, że miejsce nie ma opisu, nikt nie wie, czy można tam wejść, a wynik wyszukiwania niewiele różni się od przypadkowego katalogu firm.
Mapsy budujemy jako kuratorski atlas miejsc wartych drogi: przyrody, lokalnej historii, nietypowej architektury, urbexu i odkryć poza oczywistą trasą. Selekcja jest częścią produktu. Nie chcemy importować wszystkiego, co ma współrzędne, i udawać, że liczba pinezek oznacza jakość.
Potrzebny jest kontekst: co to za miejsce, dlaczego przyciąga uwagę i czego można się spodziewać. Społeczność może go uzupełniać, ale publiczny atlas wymaga odpowiedzialności za publikację. Prywatny zapis użytkownika ma zupełnie inny ciężar niż rekomendacja pokazywana wszystkim.
Mapsy pozostają projektem własnym w fazie alpha. Docelowa domena to mapsy.space; aplikacja nie jest jeszcze publicznie dostępna. Opisujemy fundament technologiczny i zasady projektowania systemu, a nie raport z wielkoskalowej produkcji. Przykłady kontraktów są ilustracją podejścia, nie dokumentacją konkretnych endpointów. Mechanizmy nadal rozwijane wskazujemy osobno.
Trzy warstwy i wyraźna odpowiedzialność
Stack obejmuje Next.js i TypeScript na froncie, NestJS po stronie API oraz PostgreSQL obsługiwany przez Prisma. Mapbox dostarcza warstwę mapową. Każdy z tych elementów rozwiązuje inny problem. Podział ma sens dopiero wtedy, gdy nie przenosimy tych samych reguł pomiędzy wszystkimi warstwami.
Frontend odpowiada za wybór obszaru, prezentację wyników, formularze i nawigację. API rozstrzyga, co użytkownik może odczytać lub zmienić. Baza przechowuje dane i pilnuje ich spójności. Mapa renderuje położenie miejsc, ale nie podejmuje decyzji, czy zgłoszenie zasługuje na publikację.
Przykład: ukrycie przycisku moderacji poprawia interfejs, lecz nie chroni operacji. Sprawdzenie uprawnień musi nastąpić również w API. Podobnie odfiltrowanie nieopublikowanego punktu w przeglądarce jest spóźnione, jeśli odpowiedź serwera już ujawniła jego treść.
Taki rozdział ułatwia rozwijanie aplikacji webowej etapami. Można zmienić panel miejsca bez przebudowy procesu moderacji. Można poprawić zapytanie do bazy bez zmiany sposobu obsługi gestów. Granice mają zmniejszać koszt zmian, a nie mnożyć abstrakcje dla samego diagramu.
Next.js spina doświadczenie produktu
Next.js jest warstwą routingu, renderowania i składania interfejsu. Atlas, strona miejsca oraz prywatna kolekcja potrzebują własnego kontekstu nawigacji. Użytkownik powinien móc otworzyć konkretny widok z linku, a nie za każdym razem zaczynać od przesuwania mapy.
Silnik mapy wymaga przeglądarki. Nie oznacza to, że cała aplikacja musi czekać na jego uruchomienie. Treść, podstawowy układ i komunikaty można oddzielić od inicjalizacji mapy. Ciężki komponent nie powinien blokować przeczytania informacji o miejscu ani obsługi podstawowej nawigacji.
Osobno trzeba rozstrzygnąć stan adresu URL. Fraza, kategoria czy wybrane miejsce mogą zasługiwać na zapis w adresie. Każdy ruch kamery już niekoniecznie. Jeśli drobne przesunięcie tworzy kolejny wpis historii, przycisk Wstecz staje się przeglądarką gestów zamiast sposobem powrotu.
Dane prywatne wymagają osobnego traktowania cache. Wynik zależny od zalogowanego użytkownika nie może trafić do wspólnej pamięci podręcznej jako publiczna odpowiedź. Decyzję o renderowaniu podejmujemy dla konkretnego widoku i jego danych. Sam wybór frameworka nie rozwiązuje granicy pomiędzy atlasem a kontem użytkownika.
NestJS porządkuje domenę, nie tylko endpointy
Backend obejmuje kilka różnych obszarów: użytkowników i autoryzację, miejsca, kolekcje, zgłoszenia, moderację oraz aktywność. W NestJS można wyznaczyć dla nich moduły domenowe. Ważniejsza od liczby katalogów jest odpowiedzialność: kto kontroluje publikację, kto własność kolekcji, a kto zapis aktywności.
Kontroler powinien przyjąć i zweryfikować wejście, uruchomić operację oraz zwrócić odpowiedź. Reguła „tylko właściciel edytuje tę kolekcję” nie powinna istnieć jako przypadkowy warunek skopiowany do kilku kontrolerów. Potrzebuje jednego, czytelnego miejsca w logice dostępu.
Tak samo publikacja miejsca może wymagać sprawdzenia jego stanu i kompletu danych. Zmiana statusu bez tych warunków tworzy skrót omijający proces. Z perspektywy backend developmentu istotne są właśnie takie granice, a nie samo wystawienie kolejnego adresu HTTP.
Nie ma tu potrzeby deklarowania mikroserwisów na zapas. Modułowy backend pozwala zachować wspólne transakcje i prostsze uruchamianie. Dalszy podział powinien wynikać z rzeczywistej potrzeby niezależnego wdrażania albo skalowania, nie z przekonania, że alpha musi od razu udawać dużą platformę.
PostgreSQL i Prisma przechowują więcej niż współrzędne
Miejsce jest powiązane z opisem, kategorią i stanem publikacji. Użytkownik może je zapisać, odwiedzić albo dodać do kolekcji. Zgłoszenie ma autora i przebieg weryfikacji. Te relacje uzasadniają relacyjny model danych znacznie lepiej niż sam fakt przechowywania szerokości i długości geograficznej.
Prisma porządkuje schemat, migracje i typowany dostęp do PostgreSQL. Nie zwalnia z projektowania ograniczeń. Jeżeli para użytkownik–miejsce ma reprezentować pojedyncze oznaczenie odwiedzin, spójność trzeba zabezpieczyć również w bazie. Dwa równoległe żądania nie muszą zobaczyć efektów swojej pracy przed zapisem.
Transakcja przydaje się tam, gdzie kilka zmian stanowi jedną operację. Zapis aktywności i wynikający z niego przyrost XP nie powinny rozjeżdżać się po ponowieniu żądania. Konkretne reguły nagradzania mogą ewoluować; zasada unikania podwójnego naliczania pozostaje przydatna niezależnie od wartości punktowych.
Wyszukiwanie przestrzenne wymaga osobnej oceny. PostgreSQL daje drogę do rozbudowy, także przez PostGIS, ale nie deklarujemy takiego rozszerzenia jako wdrożonej części Mapsy. Wybór indeksów i sposobu zapytania powinien wynikać z rzeczywistych danych oraz planu wykonania. Sam ORM nie jest strategią wydajności.
Dane miejsca nie są obiektem mapy
Rekord miejsca powinien zachować znaczenie także bez Mapboxa. Jego identyfikator, treść, położenie i widoczność należą do domeny aplikacji. Kolor symbolu, wielkość ikony i reguła pojawiania się etykiety należą do prezentacji.
Na potrzeby mapy można przygotować lekki zestaw punktów, na przykład GeoJSON z identyfikatorem i kategorią. Pełny opis, zdjęcia i relacje użytkowników są potrzebne dopiero przy szczegółach. Wysyłanie ich dla każdej pinezki zwiększa transfer, przetwarzanie JSON-u i koszt utrzymywania danych w pamięci.
Adapter pomiędzy odpowiedzią API a źródłem mapowym jest niewielkim, ale użytecznym rozdzieleniem. Pozwala zmienić symbol lub format prezentacji bez migracji rekordów miejsc. Ułatwia też ocenę ewentualnej zmiany dostawcy: dane produktu pozostają nasze, choć sam interfejs nadal wymagałby pracy.
Trzeba przy tym pilnować konwencji współrzędnych. GeoJSON używa kolejności długość, szerokość geograficzna. Zamieniona para może dać poprawne liczby i zupełnie niewłaściwy punkt. Typowanie pomaga opisać kontrakt, lecz kontrola zakresów i sensowne przypadki testowe nadal są potrzebne.
Szukamy w obszarze, który użytkownik ogląda
Viewport to widoczny fragment mapy. Jego granice można przekazać jako bounding box: zachód, południe, wschód i północ. Zapytanie powinno łączyć ten obszar z kategoriami i frazą. Pobranie całego atlasu tylko po to, żeby przeglądarka odrzuciła większość punktów, szybko przestaje się opłacać.
Przykładowy kontrakt może zawierać bbox, frazę, filtry i limit. API powinno sprawdzić zakres współrzędnych, dozwolone filtry oraz maksymalną liczbę wyników. Limit musi mieć czytelne znaczenie: jeśli odpowiedź jest ucięta, interfejs nie może sugerować, że pokazuje wszystkie miejsca w okolicy.
Nie wysyłamy zapytania na każdą klatkę animacji. Sensownym momentem jest zakończenie przesuwania, krótkie opóźnienie albo świadome „Szukaj w tym obszarze”. Wybór wpływa na odczuwalną kontrolę. Automatyczne odświeżanie jest wygodne, dopóki nie przestawia wyników w trakcie czytania.
Pozostaje wyścig odpowiedzi. Żądanie dla poprzedniego obszaru może skończyć się później niż nowe. Anulowanie wcześniejszego pobierania lub sprawdzenie identyfikatora żądania chroni przed podmianą aktualnych wyników. Osobno trzeba obsłużyć błąd sieci: brak odpowiedzi nie oznacza braku miejsc.
Przy małym przybliżeniu nawet ograniczenie obszaru nie wystarczy. Potrzebne mogą być agregacja, klastry po stronie serwera albo kafelki z danymi. To kierunki rozbudowy zależne od skali, nie deklaracja obecnej infrastruktury. Najpierw mierzymy rozmiar odpowiedzi, czas zapytania i pracę przeglądarki.
Markery i klastry mają koszt
Osobny element HTML dla każdego punktu daje dużą swobodę wyglądu. Daje też koszt DOM-u, zdarzeń i aktualizacji. Nie należy zakładać, że rozwiązanie wygodne dla kilkunastu markerów zachowa się tak samo przy znacznie gęstszej mapie.
Dla większych zbiorów naturalnym wariantem są źródła i warstwy renderowane przez silnik mapy. Mapbox udostępnia również klastrowanie punktów GeoJSON. Grupa zastępuje nachodzące na siebie symbole, a zbliżenie pozwala przejść do poszczególnych miejsc. Nie wymaga to stawiania osobnego komponentu React dla każdej pinezki.
Klaster nie naprawia jednak zbyt dużego transferu. Jeżeli wszystkie dane trzeba najpierw pobrać, koszt sieci już ponieśliśmy. Filtrowanie na serwerze i renderowanie w przeglądarce rozwiązują różne części problemu. Dodatkowo licznik klastra opisuje przekazany zbiór, więc ucięta odpowiedź wymaga ostrożnej komunikacji.
Instancja mapy powinna przeżyć zmianę filtrów; nie ma powodu tworzyć jej ponownie przy każdym renderze. Aktualizujemy źródło danych i stan zaznaczenia. Przy diagnozie płynności rozdzielamy czas API, przetwarzanie odpowiedzi, aktualizację źródła i renderowanie panelu. Inaczej każda przycinka zostaje niesłusznie nazwana „wolną mapą”.
Dlaczego wybraliśmy Mapbox
Przed wyborem przeanalizowaliśmy dostępne warianty i ich konsekwencje. Potrzebowaliśmy kontroli nad wyglądem, pracy na własnych danych i narzędzi, które pozwolą dopasować mapę do produktu. Nie prowadziliśmy wielomiesięcznego przetargu. Porównywaliśmy zakres integracji oraz to, co trzeba będzie utrzymywać samodzielnie.
Google Maps oferuje mapy, personalizację, własne warstwy oraz usługi związane z miejscami i geokodowaniem. To sensowna opcja, zwłaszcza gdy produkt korzysta z ekosystemu Google. Nie odrzucamy jej jako „mapy bez możliwości stylowania”. Trzeba natomiast osobno ocenić zakres usług, zasady wykorzystania danych i sposób rozliczania. Zakres interfejsu opisuje dokumentacja Maps JavaScript API.
OpenStreetMap i MapLibre oznaczają inny podział odpowiedzialności. OSM dostarcza dane geograficzne. MapLibre GL JS jest silnikiem renderowania map. Żadne z nich osobno nie składa kompletnej infrastruktury aplikacji. Potrzebne są jeszcze kafelki, styl, hosting i ewentualny geokoder. Można je kupić od dostawcy albo utrzymywać samodzielnie. To daje kontrolę, ale wymaga pracy operacyjnej; zakres silnika opisuje dokumentacja MapLibre.
Publiczny serwer kafelków OSM nie jest darmowym zapleczem dowolnie obciążanej aplikacji. Ma własną politykę użycia. Podobnie usługi geokodowania mają osobne limity. Otwarte dane nie znoszą kosztu serwerów, transferu ani aktualizacji. Pozostają też obowiązki wynikające z licencji i atrybucji.
Mapbox wybraliśmy ze względu na kontrolę stylu, dobre SDK, źródła i warstwy danych, markery, klastry oraz dostępne geokodowanie. Ten zestaw pasuje do kuratorskiego atlasu z własnym językiem wizualnym. Korzystamy z Mapboxa, ale nie traktujemy go jako gotowej bazy naszych miejsc ani zamiennika backendu.
Cena tej wygody obejmuje vendor lock-in. Style, integracja SDK i wykorzystane usługi zwiększają koszt zmiany dostawcy. Trzeba kontrolować użycie API, ograniczać uprawnienia tokenów i śledzić aktualne warunki. Cennik Mapbox rozdziela produkty; geokodowanie nie jest po prostu dodatkiem bez kosztu do każdej mapy. Nie podajemy stałego budżetu bez scenariusza ruchu. Dla innego produktu MapLibre z zewnętrznym dostawcą kafelków lub Google mogą być lepszym wyborem.
Konto użytkownika nie daje prawa do wszystkiego
Autoryzacja w Mapsy opiera się na JWT przechowywanym w cookie. Samo miejsce przechowywania tokenu nie załatwia bezpieczeństwa. Cookie sesyjne wymaga świadomej konfiguracji HttpOnly, Secure, SameSite, czasu życia i zakresu domeny. Przy żądaniach zmieniających stan trzeba uwzględnić CSRF i sposób komunikacji frontendu z API.
Uwierzytelnienie odpowiada na pytanie, kto wysyła żądanie. Uprawnienia określają, czy może wykonać daną operację. Moderator potrzebuje dostępu do zgłoszeń; właściciel kolekcji do jej edycji. Zwykły użytkownik nie uzyskuje tych praw przez podanie identyfikatora zasobu w adresie.
Kontrola własności musi objąć także odczyt. Prywatna kolekcja nie może pojawić się w publicznym endpointcie tylko dlatego, że ktoś odgadł jej identyfikator. Publiczne udostępnienie powinno być jawną decyzją z określonym zakresem widoczności, a nie przypadkowym skutkiem istnienia linku.
Zgłoszenie miejsca i prywatna kolekcja to różne procesy
Zapis „chcę tu pojechać” nie jest kandydatem do globalnej publikacji. Kolekcje służą osobistemu porządkowaniu miejsc i dzieleniu się wybranym zestawem. Atlas ma własne kryteria doboru. Połączenie tych mechanizmów oznaczałoby, że prywatne notatki zaczynają udawać redakcyjną rekomendację.
W procesie zgłoszenia potrzebne są dane pozwalające ocenić lokalizację i opis. Moderacja powinna umożliwiać akceptację, odrzucenie albo skierowanie do poprawy, z czytelną informacją o wyniku. To model procesu, który dopracowujemy w alpha; nie obietnica automatycznej weryfikacji wszystkich treści.
Przy urbexie szczególnie łatwo pomylić atrakcyjność miejsca z prawem dostępu. Opis nie powinien sugerować, że pinezka oznacza zgodę właściciela lub bezpieczeństwo wejścia. Kontekst i jakość treści są tutaj częścią użyteczności atlasu, nie ozdobnikiem pod zdjęciem.
Telefon zmienia sposób korzystania z mapy
Na desktopie mapa i lista mogą działać obok siebie. Na telefonie konkurują o ten sam ekran. Wysuwany panel musi zostawić miejsce na orientację w terenie, a po otwarciu szczegółów użytkownik powinien rozumieć, który punkt wybrał i jak wrócić do przeglądania.
Geolokalizacja powinna odpowiadać na świadomą akcję. Odmowa uprawnień musi pozostawić działającą mapę i możliwość wskazania okolicy ręcznie. Odczyt pozycji ma też dokładność, więc kropka na ekranie nie jest dowodem fizycznej obecności przy konkretnym obiekcie.
Projektowanie UX/UI obejmuje tu wielkość celów dotykowych, zachowanie paneli, czytelność na zewnątrz i komunikaty podczas słabego połączenia. Lista miejsc daje alternatywny sposób poruszania się po wynikach. Nie obiecujemy trybu offline tylko dlatego, że mapa działa w telefonie.
Alpha zostawia miejsce na kolejne etapy
Mapsy rozwijamy wokół mapy, atlasu i aktywności użytkowników. Ulubione, kolekcje, udostępnianie, zgłoszenia, oznaczenie „Byłem tutaj”, relacje, XP i osiągnięcia wymagają spójnych zasad. Samo dodanie przycisków nie kończy tych funkcji. Mechanizm Moments, czyli krótkich aktualizacji z miejsca, pozostaje w rozwoju.
Aktywność ma zachęcać do odkrywania, a nie nagradzać seryjne klikanie. Dlatego reguły XP, widoczność relacji i ograniczanie nadużyć trzeba projektować razem. Nie przypisujemy im dziś niepotwierdzonych wyników ani wpływu na utrzymanie użytkowników.
Następne decyzje powinny wynikać z testowania rzeczywistych scenariuszy: znalezienia okolicy, wyboru miejsca, zapisania planu i powrotu po wizycie. Architektura ma pozwalać poprawiać te kroki osobno. Mapa jest najbardziej widoczną warstwą Mapsy. Wiarygodność produktu zależy również od danych, dostępu, publikacji i tego, co dzieje się po dotknięciu pinezki.
Aplikacje webowe