Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik11 min czytania

PocketBase, całe zaplecze w jednym pliku

PocketBase to baza, uwierzytelnianie, pliki i API w jednym pliku wykonywalnym. Reguły dostępu, rozszerzanie w Go, ograniczenia i status przed wersją 1.0.

PocketBase, czyli zaplecze mieszczące się w jednym pliku

Pomysł brzmi nieprawdopodobnie, dopóki nie zobaczysz go w działaniu. Pobierasz jeden plik wykonywalny, uruchamiasz go i masz bazę danych, uwierzytelnianie z logowaniem przez zewnętrznych dostawców, przesyłanie plików, powiadomienia w czasie rzeczywistym, panel administracyjny i gotowe API.

Bez instalatora, bez zależności, bez konfiguracji usług. Jeden plik i katalog z danymi obok niego.

Code
Bash
./pocketbase serve

Panel działa od razu, API również, a pierwsza kolekcja danych powstaje w minutę. Dla prototypu, aplikacji wewnętrznej albo projektu pobocznego to jest oszczędność liczona w dniach, nie w godzinach.

Zastrzeżenie, które trzeba postawić na początku

Projekt nie osiągnął jeszcze wersji pierwszej i sam o tym uprzedza. Dokumentacja mówi wprost, że pełna zgodność wsteczna nie jest gwarantowana przed wersją 1.0, a autor odradza używanie go w zastosowaniach krytycznych, jeśli nie odpowiada Ci czytanie listy zmian i wykonywanie ręcznych kroków przy aktualizacjach.

To jest uczciwe postawienie sprawy i warto je uszanować, zamiast dowiadywać się o tym przy trzeciej aktualizacji.

Praktyczny wniosek nie brzmi „nie używaj", tylko „używaj świadomie". Aplikacja wewnętrzna, panel dla dziesięciu osób, prototyp pokazywany inwestorowi, narzędzie dla jednego zespołu: tam ryzyko jest znikome, a zysk realny. System obsługujący płatności klientów zewnętrznych to inna rozmowa.

Przypinaj wersję i czytaj listę zmian przed każdą aktualizacją. To dwa nawyki, które sprowadzają całe ryzyko do drobnej niedogodności.

Kolekcje i reguły dostępu

Struktura danych powstaje w panelu albo przez migracje, a każda kolekcja niesie własne reguły określające, kto może co zrobić.

Reguła to wyrażenie porównujące pola dokumentu z danymi zalogowanego użytkownika. Wygląda niepozornie i jest tu najważniejszym mechanizmem bezpieczeństwa.

Code
TEXT
listRule:   @request.auth.id != ""
viewRule:   @request.auth.id != ""
createRule: @request.auth.id != ""
updateRule: autor = @request.auth.id
deleteRule: autor = @request.auth.id

Ta piątka mówi, że zalogowany widzi wszystko, a zmieniać i usuwać może wyłącznie własne wpisy. Stany reguły są trzy i to one myli się najczęściej. Reguła nieustawiona, oznaczona w panelu kłódką, dopuszcza wyłącznie superużytkownika i tak wygląda stan domyślny każdej nowej kolekcji. Reguła odblokowana i pozostawiona pusta wpuszcza każdego, łącznie z klientem bez tokenu. Reguła zawierająca wyrażenie wpuszcza tego, kto je spełnia.

Kierunek najczęstszej pomyłki jest tu odwrotny, niż podpowiada intuicja. Kolekcja nietknięta nie wycieka, bo domyślnie jest zamknięta. Wycieka ta, którą ktoś odblokował, żeby prototyp w ogóle ruszył, i zostawił puste pole zamiast wyrażenia. Kolekcja z danymi osobowymi odsłonięta w ten sposób i zapomniana pozostaje otwarta na świat.

Warto sprawdzić to bezpośrednio, wywołując API bez tokenu uwierzytelniającego, zanim cokolwiek trafi do internetu. Jedno polecenie odpowiada na pytanie, na które panel odpowiada niejednoznacznie.

Code
Bash
curl -s "http://127.0.0.1:8090/api/collections/zadania/records" | head -c 300

Jeśli w odpowiedzi zobaczysz rekordy, a nie pustą listę albo błąd 403, kolekcja jest otwarta dla każdego. Reguła zamknięta zwraca 403, a niespełniona reguła listowania zwraca 200 z pustą tablicą, więc rozróżnienie widać dopiero w treści odpowiedzi. To jest test, który warto wpisać do listy kontrolnej przed wdrożeniem i powtarzać po każdej zmianie reguł, bo panel pokazuje reguły, a nie ich skutek.

Uruchomienie samego serwera i założenie pierwszego konta administracyjnego to dwa polecenia, bez instalatora i bez zależności.

Code
Bash
./pocketbase superuser create admin@example.com haslo-min-8-znakow
./pocketbase serve --http=127.0.0.1:8090

Klient i praca w czasie rzeczywistym

Biblioteka kliencka ukrywa szczegóły API i daje jeden spójny interfejs.

Code
TypeScript
import PocketBase from 'pocketbase'

const pb = new PocketBase('http://127.0.0.1:8090')

await pb.collection('users').authWithPassword('anna@example.com', 'haslo')

const zadania = await pb.collection('zadania').getList(1, 20, {
  filter: 'zakonczone = false',
  sort: '-created',
  expand: 'przypisany'
})

Subskrypcja zmian to jedna linia i działa bez konfigurowania czegokolwiek po stronie serwera.

Code
TypeScript
pb.collection('zadania').subscribe('*', (e) => {
  if (e.action === 'create') dodajDoListy(e.record)
  if (e.action === 'update') odswiez(e.record)
})

Warto wiedzieć, jak to działa pod spodem, bo ma to konsekwencje. Powiadomienia idą przez strumień zdarzeń wysyłany przez serwer, czyli zwykłe długotrwałe połączenie HTTP, a nie przez gniazda dwukierunkowe. Jest to prostsze i lepiej przechodzi przez pośredniki sieciowe, natomiast każde otwarte połączenie zajmuje zasoby, więc przy tysiącach jednoczesnych klientów warto to zmierzyć.

Rozwinięcie powiązań przez parametr rozszerzenia oszczędza dodatkowe zapytania i jest jedną z tych rzeczy, których brak zauważa się dopiero przy wolno działającej liście. Bez niego lista dwudziestu zadań z przypisanymi osobami generuje dwadzieścia jeden zapytań zamiast jednego, a różnica rośnie proporcjonalnie do długości listy.

Rozszerzanie w Go

Tu leży różnica między zabawką a narzędziem, którego można użyć poważnie. PocketBase jest programem i w równym stopniu biblioteką, więc własną logikę dopisujesz w kodzie i budujesz swój plik wykonywalny.

Code
Go
package main

import (
    "log"
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()

    app.OnRecordAfterCreateSuccess("zamowienia").BindFunc(func(e *core.RecordEvent) error {
        wyslijPotwierdzenie(e.Record.GetString("email"))
        return e.Next()
    })

    if err := app.Start(); err != nil {
        log.Fatal(err)
    }
}

Zdarzenia obejmują wszystkie operacje na rekordach, uwierzytelnianie i obsługę żądań, więc walidacja, powiadomienia i integracje mają gdzie się znaleźć. Można też dodać własne trasy API obok tych generowanych automatycznie.

To rozwiązuje problem typowy dla gotowych systemów zaplecza: co zrobić, gdy potrzebujesz czegoś, czego autor nie przewidział. Tutaj odpowiedzią jest napisanie tego w Go, a nie szukanie obejścia.

Ceną jest to, że własna logika wymaga znajomości Go i osobnego procesu budowania. Jeśli zespół pisze wyłącznie w językach przeglądarki, ta część pozostanie nieużywana, a wtedy część przewagi znika.

Uwierzytelnianie i konta użytkowników

Warstwa logowania jest tu gotowa i to jedna z rzeczy, które najbardziej skracają czas do pierwszej działającej wersji.

Kolekcja użytkowników istnieje od początku i obsługuje logowanie hasłem, potwierdzenie adresu pocztowego, odzyskiwanie hasła oraz logowanie przez zewnętrznych dostawców tożsamości. Włączenie logowania przez konto w popularnym serwisie to wpisanie dwóch wartości w panelu, bez pisania kodu.

Code
TypeScript
await pb.collection('users').authWithOAuth2({ provider: 'google' })

Token trafia do pamięci klienta i jest automatycznie dołączany do kolejnych zapytań, a jego odświeżanie biblioteka bierze na siebie. Warto natomiast wiedzieć, gdzie ten token siedzi, bo domyślne przechowywanie w pamięci przeglądarki oznacza, że skrypt wstrzyknięty na stronę może go odczytać.

Można utworzyć więcej niż jedną kolekcję kont, i to bywa przydatne. Osobna kolekcja dla klientów i osobna dla pracowników pozwala trzymać różne pola i różne reguły, zamiast upychać wszystkich w jednej tabeli z polem roli.

Rola użytkownika to zwykłe pole, którego wartość trzeba chronić regułą. Pole roli edytowalne przez właściciela rekordu pozwala każdemu awansować się na administratora jednym zapytaniem, a to jest błąd popełniany częściej, niż mogłoby się wydawać.

Pliki, migracje i praca zespołowa

Trzy rzeczy, które przy prototypie nie mają znaczenia, a przy projekcie żyjącym dłużej niż miesiąc decydują o komforcie pracy.

Przesyłanie plików jest wbudowane: pole odpowiedniego typu przyjmuje załączniki, a serwer generuje miniatury obrazów w podanych rozmiarach. Pliki lądują domyślnie na dysku obok bazy, co jest zaletą przy kopii zapasowej i wadą przy braku miejsca. Da się skierować je do magazynu obiektowego, i przy większych zbiorach warto zrobić to od początku.

Code
TypeScript
const formularz = new FormData()
formularz.append('tytul', 'Raport kwartalny')
formularz.append('zalacznik', plik)

const rekord = await pb.collection('dokumenty').create(formularz)
const miniatura = pb.files.getURL(rekord, rekord.zalacznik, { thumb: '200x200' })

Miniatury generują się na żądanie przy pierwszym wywołaniu z danym rozmiarem, więc lista z podglądami jest wolna tylko raz. Warto trzymać się kilku ustalonych rozmiarów zamiast wyliczać je z szerokości okna, bo każdy nowy rozmiar to osobny plik na dysku.

Migracje schematu są zapisywane automatycznie, gdy zmieniasz strukturę w panelu. Powstaje plik, który trafia do repozytorium i wykonuje się przy starcie na innym środowisku. Dzięki temu zmiana wprowadzona lokalnie dociera na produkcję razem z kodem, a nie przez klikanie w panelu produkcyjnym.

Ten mechanizm działa dobrze pod jednym warunkiem: nikt nie zmienia struktury bezpośrednio na produkcji. Zmiana wprowadzona tam ręcznie nie ma odpowiednika w repozytorium i przy kolejnym wdrożeniu potrafi zostać nadpisana albo wywołać konflikt.

Praca zespołowa nad jednym plikiem bazy jest niemożliwa, więc każdy programista uruchamia własną instancję lokalnie. To akurat działa dobrze, bo uruchomienie jest natychmiastowe, a dane testowe da się trzymać jako zestaw do wgrania po starcie.

Wdrożenie i kopie zapasowe

Uruchomienie jest tak proste, jak obiecuje opis: wgrywasz plik na serwer, ustawiasz usługę systemową i pośrednika obsługującego certyfikat. Jedna maszyna za kilka dolarów miesięcznie obsłuży aplikację z tysiącami użytkowników, bo baza jest wbudowana i nie ma ruchu sieciowego między procesami.

Konsekwencją tej prostoty jest brak skalowania poziomego. Nie da się uruchomić trzech instancji przed rozdzielaczem ruchu, bo każda miałaby własny plik bazy. Skalowanie odbywa się w pionie, przez mocniejszą maszynę, i przy większości aplikacji to wystarcza dłużej, niż podpowiada intuicja.

Kopie zapasowe są w tym układzie wyjątkowo proste, bo cały stan to katalog z plikiem bazy i przesłanymi plikami. System ma wbudowany mechanizm kopii z wysyłaniem do magazynu zewnętrznego i warto go włączyć pierwszego dnia, a nie po pierwszej awarii.

Sprawdź też odtworzenie kopii, zanim będzie potrzebne. Kopia, której nikt nigdy nie odtworzył, jest kopią wyłącznie z nazwy, a to zdanie kosztowało już wiele zespołów bardzo dużo.

PocketBase kontra alternatywy

RozwiązanieMocna stronaSłabośćKiedy wybrać
PocketBaseJeden plik, zero konfiguracji, tani hostingPrzed wersją 1.0, brak skalowania poziomegoPrototyp, narzędzie wewnętrzne, projekt poboczny
SupabasePostgres, dojrzałość, usługa zarządzanaWięcej ruchomych częściAplikacja produkcyjna dla klientów
AppwriteSzeroki zakres funkcji, wdrożenie u siebieCięższe wdrożenie, kilka kontenerówZespół chcący wszystkiego u siebie
ConvexLogika i dane w jednym modeluRozliczenie za użycie, mniejsza społecznośćAplikacja mocno interaktywna

Rozstrzygnięcie sprowadza się do jednego pytania: czy prostota jest tu wartością, czy ryzykiem. Przy narzędziu dla zespołu, prototypie i aplikacji o przewidywalnej skali prostota wygrywa wszystko inne. Przy systemie, który ma rosnąć i obsługiwać klientów zewnętrznych, dojrzalsze rozwiązanie daje spokój wart swojej ceny.

Warto też pamiętać, że migracja stąd nie jest dramatyczna. Dane siedzą w zwykłej bazie plikowej, którą da się wyeksportować, a struktura kolekcji przekłada się na tabele bez tłumaczenia pojęć.

Wydajność i limity

Baza wbudowana ma inne charakterystyki niż baza sieciowa i warto je znać, zanim aplikacja urośnie.

Odczyty są tu bardzo szybkie, bo nie ma opóźnienia sieciowego ani serializacji między procesami. Zapytanie kończy się w ułamku milisekundy, co przy typowej aplikacji oznacza, że wąskim gardłem staje się cokolwiek innego, zwykle renderowanie po stronie klienta.

Zapisy mają inną naturę. Baza plikowa obsługuje jednego pisarza naraz, więc równoległe zapisy ustawiają się w kolejkę. Przy zwykłym ruchu to niezauważalne, natomiast masowy import kilkuset tysięcy rekordów wykonany rekord po rekordzie potrafi trwać godzinami. Wsadowe zapisy w jednej transakcji skracają to do minut.

Warto też pilnować indeksów. Kolekcja z dziesięcioma tysiącami rekordów działa szybko niezależnie od tego, co zrobisz, a przy milionie brakujący indeks na polu użytym do filtrowania zamienia listę w ekran ładowania. Panel pozwala dodać indeks bez pisania zapytań.

Ostatnia rzecz to rozmiar pliku bazy. Rośnie razem z danymi i razem z historią zmian, a przy plikach przechowywanych obok potrafi urosnąć szybciej, niż zakłada plan przestrzeni dyskowej. Monitorowanie wolnego miejsca na tej jednej maszynie jest tu ważniejsze niż przy rozwiązaniach rozproszonych, bo brak miejsca zatrzymuje wszystko naraz.

Typowe błędy

Pierwszy to odblokowanie reguły i zostawienie jej pustej. Pusta reguła oznacza dostęp dla każdego, więc dane odsłonięte przy prototypie zostają otwarte, choć kolekcja nietknięta byłaby zamknięta.

Drugi to używanie w systemie krytycznym mimo ostrzeżenia autora. Projekt przed wersją pierwszą nie gwarantuje zgodności wstecznej i mówi o tym wprost.

Trzeci to aktualizacja bez czytania listy zmian. To jedno z niewielu miejsc, gdzie ten nawyk naprawdę oszczędza wieczór.

Czwarty to planowanie skalowania poziomego. Architektura z wbudowaną bazą tego nie przewiduje, więc plan zakładający kilka instancji trzeba porzucić na etapie projektu.

Piąty to brak kopii zapasowych mimo wbudowanego mechanizmu. Włączenie go zajmuje minutę, a cały stan aplikacji siedzi w jednym katalogu.

Szósty to przenoszenie logiki biznesowej do przeglądarki, bo tak szybciej. Reguły dostępu pilnują danych, ale nie zastąpią walidacji, a ta należy do zdarzeń po stronie serwera.

FAQ

Czy PocketBase nadaje się na produkcję?

Zależy od tego, co produkcja oznacza w Twoim przypadku. Do narzędzi wewnętrznych, aplikacji dla zespołu i projektów o przewidywalnej skali tak, przy przypiętej wersji i czytaniu listy zmian. Autor odradza go w zastosowaniach krytycznych, dopóki projekt nie osiągnie wersji pierwszej.

Czy poradzi sobie z dużym ruchem?

Jedna maszyna obsłuży zaskakująco dużo, bo baza jest wbudowana i nie ma opóźnień sieciowych między procesami. Ograniczeniem jest brak skalowania poziomego: rośniesz mocniejszą maszyną, a nie kolejnymi instancjami.

Czym różni się od Supabase?

Skalą i dojrzałością. Supabase daje Postgresa, usługę zarządzaną i szerszy zestaw funkcji, kosztem większej złożoności. PocketBase daje jeden plik i zero konfiguracji, kosztem statusu przed wersją pierwszą i braku skalowania poziomego.

Czy muszę znać Go?

Do podstawowego użycia nie, bo panel i API działają bez pisania kodu serwerowego. Znajomość Go otwiera natomiast rozszerzanie o własną logikę i trasy, co jest tu największą przewagą nad podobnymi rozwiązaniami.

Jak wygląda kopia zapasowa?

Cały stan to katalog z plikiem bazy i przesłanymi plikami, więc kopia oznacza skopiowanie tego katalogu. System ma wbudowany mechanizm kopii z wysyłaniem do magazynu zewnętrznego i warto włączyć go od razu, razem ze sprawdzeniem odtworzenia.

Dokumentacja stoi na stronie projektu, a kod w repozytorium na GitHubie.