Prowadzę jednoosobową działalność i nie używam programu księgowego. Przez długi czas nie był to problem. Faktury przychodziły mailem, zapisywałem je i przekazywałem księgowej.
Sytuacja zmieniła się wraz z przejściem polskiego obrotu B2B do centralnego systemu e-faktur. Faktura kosztowa może pojawić się w systemie, ale nie w mojej skrzynce. Potrzebowałem więc prostego narzędzia, które dwa razy dziennie sprawdzi, czy pojawiły się nowe dokumenty, przygotuje ich czytelną wersję PDF i wyśle jedno powiadomienie.
Tak powstał KSeF Watch.
Sam pomysł był niewielki. Projekt szybko pokazał jednak, że mała aplikacja może wymagać całkiem poważnych decyzji dotyczących architektury, bezpieczeństwa i odpowiedzialności.
Najpierw projekt, potem kod
Nie zacząłem od generowania ekranów ani pisania pierwszego endpointu. Najpierw powstał dokument projektowy: architektura, model danych, sposób wdrożenia, zagrożenia, ograniczenia platformy oraz dziennik decyzji.
Na końcu dopisałem listę założeń przyjętych bez wyraźnego potwierdzenia. Było ich czterdzieści.
Część dotyczyła techniki, część produktu, prawa lub zachowania użytkowników. Wiele liczb wyglądało w dokumencie jak przemyślane decyzje: czas życia sesji, okres retencji plików, częstotliwość synchronizacji czy limit załączników. Po zebraniu ich w jednym miejscu było widać, że część z nich była tylko rozsądnym przypuszczeniem.
To właśnie odróżnia dla mnie pracę wspieraną przez AI od prostego vibecodingu. Nie chodzi o to, ile kodu napisał model. Ważne jest, czy po projekcie zostaje jedynie działająca aplikacja, czy także decyzje, założenia i ryzyka, które można zakwestionować.
Projekt poboczny nie musi być SaaS-em
Dopiero po przygotowaniu projektu dokładniej sprawdziłem rynek. To była zła kolejność.
Znalazłem kilka produktów robiących praktycznie to samo. Niektóre były bardzo tanie, inne darmowe, a duże systemy księgowe zaczęły dodawać podobną funkcję do swoich standardowych planów.
Wniosek biznesowy był prosty: nie ma sensu tworzyć kolejnego małego SaaS-u opartego wyłącznie na tej funkcji.
Mimo to zbudowałem aplikację. Zmienił się tylko cel. Projekt miał rozwiązać mój problem, sprawdzić określony sposób pracy i dostarczyć publiczny przykład kodu oraz decyzji architektonicznych.
Usunąłem plany taryfowe, billing i cele wzrostu. Nie zniknęły jednak koszty ani odpowiedzialność.
Aplikacja nadal zależy od zewnętrznego systemu państwowego. Gdy zmieni się API, ktoś musi poprawić integrację. Gdy przestanie działać uwierzytelnienie, ktoś musi znaleźć przyczynę. Gdy zadanie cykliczne się nie wykona, ktoś powinien zauważyć problem, zanim użytkownik zacznie szukać brakującej faktury.
Hosting, poczta, przechowywanie danych i monitoring mogą być tanie, ale nie są całkowicie bezpłatne. Ważniejszy jest jednak koszt odpowiedzialności. Narzędzie obsługuje poświadczenia dające dostęp do dokumentów finansowych. Fakt, że użytkownik nic nie płaci, nie zmniejsza znaczenia bezpieczeństwa.
Darmowe dla użytkownika nie oznacza darmowe dla autora.
Dlaczego wybrałem Cloudflare
Jestem dużym fanem Cloudflare. Nie jest to reklama i nie otrzymuję z tego żadnej prowizji.
Platforma oferuje bardzo dużo infrastruktury w korzystnej cenie: Workers, kolejki, bazę D1, KV, R2, renderowanie przez zarządzaną przeglądarkę i usługi pocztowe. Dla małej aplikacji oznacza to możliwość uruchomienia całości bez budowania rozbudowanego środowiska chmurowego.
Niski koszt infrastruktury nie oznacza jednak braku kompromisów. Część kosztu przenosi się z utrzymania środowiska na projekt i implementację.
KSeF Watch od początku był projektowany dla Cloudflare Workers. Nie migrował z Node.js. Ograniczenia edge runtime musiały więc zostać uwzględnione przed napisaniem kodu.
Podczas uwierzytelniania zewnętrzny system udostępniał certyfikat
publiczny X.509, natomiast WebCrypto oczekiwało klucza w formacie
SPKI. Certyfikatu nie dało się przekazać bezpośrednio do
crypto.subtle.importKey(). Trzeba było go sparsować,
wyciągnąć klucz i dopiero wtedy wykonać import.
Przetwarzanie XML wymagało podobnej ostrożności. Runtime nie
udostępniał przeglądarkowego DOMParser, a bibliotek
przeznaczonych dla Node.js nie można było wybierać bez sprawdzenia
ich zależności. Parser XML stał się decyzją platformową, a nie
drobnym szczegółem implementacji.
Poczta wymagała jeszcze większej zmiany. Klasyczny klient SMTP nie był naturalnym rozwiązaniem, ponieważ runtime nie udostępnia standardowego środowiska TCP oczekiwanego przez takie biblioteki. Wysyłka musiała korzystać z API HTTP albo natywnej usługi platformy.
Limity wykonania wpłynęły również na przetwarzanie w tle. Pobranie wielu faktur, wygenerowanie PDF-ów i wysłanie wszystkiego w jednym zadaniu mogłoby zużyć zbyt dużo czasu CPU i wykonać zbyt wiele wywołań zewnętrznych. Pracę trzeba było podzielić na mniejsze jednostki i przekazywać przez kolejki.
Cloudflare nadal był dla tego projektu dobrym wyborem. Trzeba było jednak zaakceptować, że tańsza infrastruktura nie zawsze oznacza prostszą implementację.
PDF bez zwykłego serwera
Generowanie PDF wygląda na prostą funkcję, dopóki aplikacja nie działa w środowisku bez lokalnego systemu plików, zainstalowanej przeglądarki i nieograniczonego czasu procesora.
Rozważałem cztery podejścia.
Zarządzana przeglądarka pozwala przygotować HTML i CSS, a następnie wydrukować widok do PDF. Daje dobrą kontrolę nad układem i naturalnie obsługuje fonty oraz znaki narodowe. Jest jednak wolniejsza i zależy od usługi platformowej, której nie zawsze da się odtworzyć lokalnie.
Bezpośrednie budowanie PDF w JavaScript jest szybkie i nie wymaga zewnętrznego renderera. W zamian aplikacja sama odpowiada za pozycjonowanie tekstu, tabele, podziały stron i osadzanie fontów. Standardowe fonty PDF nie zawierają wszystkich znaków, więc dla języka polskiego potrzebny może być własny font.
Kontener z klasycznym narzędziem daje największą swobodę, ale wprowadza dodatkowy obraz do utrzymania, cięższe wdrożenie i możliwe zimne starty. Dla małej aplikacji usuwa część prostoty, dla której wybrano edge.
Zewnętrzna usługa PDF jest najszybsza do wdrożenia, ale staje się kolejnym dostawcą i kolejnym miejscem przetwarzania danych z faktur.
Wybrałem zarządzaną przeglądarkę, ukrytą za prostym interfejsem. Reszta aplikacji nie musi wiedzieć, w jaki sposób powstaje dokument. W razie potrzeby renderer można później wymienić.
Ważny problem pojawił się dopiero przy analizie całego procesu dostarczenia. PDF miał trafić do maila, a usługa pocztowa ogranicza rozmiar całej wiadomości do 5 MiB. Kodowanie base64 zwiększa rozmiar załączników mniej więcej o jedną trzecią, więc faktyczny budżet jest niższy.
Pierwsze szacunki również okazały się nietrafione. Zakładałem około 200 KB na dokument. Prosta wizualizacja bez grafiki zajmowała raczej 20–50 KB. Jedno rzeczywiste mierzenie zmieniło projekt bardziej niż wcześniejsze pomysły dotyczące kompresji i dzielenia wiadomości.
Najpierw wygeneruj prawdziwy dokument. Potem go zmierz.
Obietnica bezpieczeństwa, której nie można złożyć
Najtrudniejsza decyzja dotyczyła poświadczeń.
KSeF Watch działa automatycznie. Zadanie w tle musi użyć poświadczenia bez obecności użytkownika. To prowadzi do prostego wniosku: jeśli aplikacja potrafi odszyfrować sekret, musi mieć dostęp do klucza, który na to pozwala.
Osoba kontrolująca wdrożenie może potencjalnie zmienić kod tak, aby sekret został ujawniony. Szyfrowanie tego faktu nie usuwa.
Może ochronić dane w bazie, kopiach zapasowych i logach. Może uniemożliwić ich odczyt pracownikowi wsparcia albo administratorowi bazy. Nie może jednak sprawić, że sekret będzie jednocześnie nieczytelny dla aplikacji i dostępny dla autonomicznego zadania.
Dlatego obietnica, że nikt — nawet administrator — nie może uzyskać dostępu do poświadczenia, jest w takim systemie zbyt mocna.
Prawdziwe zero-knowledge wymagałoby obecności użytkownika przy każdym odszyfrowaniu. To może działać w aplikacji interaktywnej, ale nie w systemie wykonującym zadania cykliczne.
Uczciwa obietnica jest węższa. Poświadczenie może być zaszyfrowane kluczem niedostępnym dla personelu operacyjnego i administratorów bazy. Odszyfrowanie może odbywać się tylko w wydzielonym komponencie, dla określonej operacji, a każde użycie może zostać zarejestrowane.
Nie sprawia to, że dostęp jest niemożliwy. Sprawia, że jest trudny, kontrolowany i widoczny.
Administrator nie powinien móc otworzyć bazy i przeczytać sekretu. Jego wydobycie powinno wymagać świadomej zmiany kodu, wdrożenia oraz pozostawienia śladu w procesie review i audycie.
To słabsza obietnica niż „nikt nigdy nie może tego odczytać”.
Jest jednak prawdziwa i możliwa do zweryfikowania.
Co zostało z małej aplikacji
KSeF Watch rozwiązał prosty problem: informuje o nowych fakturach i dostarcza ich czytelną wersję.
Najcenniejszym rezultatem nie okazała się jednak sama funkcja.
Projekt pokazał, że tani stack może wymagać bardziej świadomego projektu, darmowe narzędzie nadal tworzy odpowiedzialność, a dobra architektura bezpieczeństwa nie polega na składaniu absolutnych obietnic.
Nie każdy projekt poboczny musi stać się SaaS-em. Może służyć do nauki, prezentować sposób pracy i rozwiązywać wąski problem.
Warto jednak wiedzieć, gdzie naprawdę znajdują się jego koszty.
Kod projektu jest dostępny publicznie: github.com/50bitSolutions/ksefwatch