Case study · PMS SaaS · 2026

Rodzinny biznes wynajmu → produkcyjny SaaSw 6 tygodni, z Claude Code jako co-pilotem

Zbudowałem od zera system zarządzania najmem krótkoterminowym (PMS) dla rodzinnego biznesu — multi-tenant, SaaS-ready, z pełną automatyzacją przepływu umów, integracjami, testami i audytem. Działa produkcyjnie, obsłużył 5 prawdziwych rezerwacji. Prototyp zajął 4 godziny, produkcja 6 tygodni „przy okazji".

4hprototyp
6 tygdo produkcji
200testów
37commitów

Kontekst

Rodzina prowadząca najem. Excel, Messenger, email — chaos.

Nieruchomość wynajmowana krótkoterminowo, rozliczana ryczałtem. Ojciec — właściciel, podpisuje umowy. Córka — koordynuje rezerwacje i obsługuje klientów, to główny aktywny użytkownik systemu.

Stan „przed": rezerwacje w Excelu, koordynacja na grupie Messengera, szablony umów rozproszone po mailach, ręczne generowanie i wysyłanie kolejnym klientom, kaskada odpowiedzi zwrotnych z podpisanymi skanami, brak miejsca na koszty i rozliczenie podatkowe.

Prosty problem, typowy dla małych firm — ale żaden komercyjny PMS nie dopasowany: za drogi, za generyczny, wielu kanałów których nie używamy, brak wsparcia dla polskiego ryczałtu.

Ból procesu

  • Rezerwacje w Excelu, łatwo o zdublowanie daty
  • Grupka Messengera jako „system koordynacji"
  • Chaos z szablonami umów — kto ma aktualną wersję?
  • Ręczne generowanie umów → wysyłka → czekanie → druk → podpis → skan → drugi mail
  • Brak widoczności kosztów, brak zestawień do PIT
  • Zero metryk rentowności

Hipoteza

Jedna osoba + LLM jako co-pilot = produkcyjny SaaS w tygodniach, nie miesiącach.

Priorytet #1

Bezpieczeństwo. Rodzinne dane klientów (PESEL, adresy, podpisane umowy) nie mogą wypłynąć. Zero half-baked security.

Priorytet #2

Pełna automatyzacja umów. Od pobrania danych gościa przez formularz, przez generowanie umowy z szablonu, wysyłkę, odesłanie podpisanej, aż po powiadomienie właściciela — zero ręcznej ingerencji.

Priorytet #3

Ship fast, iteruj. Nie enterprise clean code — good-enough z testami. Deploy na produkcję cały czas, feedback od realnych użytkowników.

Core feature

Automatyzacja przepływu umów — od zapytania do archiwum

Centralny flow całego systemu. Córka tworzy rezerwację, dalej wszystko dzieje się samo — włącznie z wysyłką podpisanej umowy do ojca, który ją kontrasygnuje.

  1. 1

    Rezerwacja wpływa

    Bezpośrednio przez admin panel lub przez formularz zapytania na stronie (widget embed). Automatyczne sprawdzenie overlap z istniejącymi terminami + logika turnover day (checkout w dniu check-in innego gościa to OK).

    actions/reservations.ts · lib/pricing · /api/public/availability

  2. 2

    Link do formularza gościa

    Córka klika „wygeneruj link", system tworzy jednorazowy token (256-bit entropy, 7 dni ważności), URL przygotowany do skopiowania (z ochroną przed trailing newline w env). Gość dostaje link i wypełnia swoje dane bez logowania.

    actions/guest-form.ts · randomBytes(32) · /formularz/[token]

  3. 3

    Gość wypełnia formularz

    Walidacja Zod (PESEL z decodePesel, email regex, rate-limit 10/15min per token). Token jest unieważniany po pierwszym użyciu — brak replay attacków.

    Zod schema · rate-limit · token invalidation after submit

  4. 4

    Automatyczne wygenerowanie umowy

    System bierze szablon umowy (domyślny dla org lub wybrany dla rezerwacji — np. „Zapłata z góry za całość"), wypełnia placeholdery ({{najemca_imie_nazwisko}}, {{data_od}}…) danymi z rezerwacji + formularza. Nowy rekord Contract w bazie, share link UUID v4.

    generateContractFromTemplateInternal · 15 placeholderów

  5. 5

    Email do gościa z linkiem

    Resend wysyła email „Umowa najmu — Domek Wiśniew (6 czerwca 2026)" z przyciskiem „Otwórz umowę" (strona /share/[token] sanityzowana przez sanitize-html, bez XSS). Każdy email trafia do EmailLog z pełną treścią HTML do podglądu w adminie.

    Resend API · sanitizeContractHtml · /share/[token] · EmailLog

  6. 6

    Gość podpisuje i odsyła PDF

    Gość otwiera link, drukuje/podpisuje tradycyjnie lub kwalifikowanym PAdES (mObywatel). Wgrywa podpisany PDF przez /share/[token]/upload (public endpoint, walidacja: tylko PDF, max 20MB, idempotentność przez shareToken). Jeśli podpis elektroniczny jest — weryfikacja PAdES przez node-forge, metadane wystawcy zapisane.

    /api/public/upload-signed-contract · Vercel Blob · node-forge PAdES

  7. 7

    Automatyczna notyfikacja właściciela

    System wysyła do ojca (properties.ownerEmail) maila z linkiem do podpisanego PDF + zielonym banerem „Podpis elektroniczny zweryfikowany" (jeśli PAdES) lub pomarańczowym „weryfikacja manualna" (jeśli skan). Ojciec otwiera, kontrasygnuje, proces zakończony.

    contract.status → GUEST_SIGNED → OWNER_SIGNED → FINALIZED

  8. 8

    Wszystko w jednym miejscu

    Cały flow widoczny w timeline rezerwacji: guest form filled, contract sent, signed, owner notified. Historia w audit logu, globalny log emaili z akcją „Wyślij ponownie" gdyby coś padło. Zero ręcznej pracy na każdym etapie.

    19 tabel Prisma · AuditLog · EmailLog · ReservationTimeline

Co potrafi system

14 modułów, grupowanych po efekcie biznesowym

Zarządzanie rezerwacjami

CRM

Lista z filtrami (status, platforma, płatność), szczegóły z tabami, ochrona przed overlapping, status workflow (NEW → CONFIRMED → WAITING_DEPOSIT → COMPLETED → CANCELLED) z dozwolonymi tranzycjami, turnover day logic.

Umowy

Core

Tiptap WYSIWYG editor, szablony ze zmiennymi (15 placeholderów), workflow statusów (DRAFT → SENT → GUEST_SIGNED → OWNER_SIGNED → FINALIZED), inline podgląd podpisanego PDF przez iframe, weryfikacja PAdES z mObywatel, IDOR guards na każdej mutacji.

Portal gościa

CRM

Samoobsługowy formularz dla gościa (imię, adres, PESEL z decodePesel), walidacja Zod, jednorazowy token 256-bit, rate-limit 10/15min, auto-invalidate po użyciu, automatyczne wygenerowanie i wysłanie umowy.

Komunikacja email

Marketing automation

5 wyzwalaczy cyklu życia (reservation created → contract sent → confirmed → pre-checkin → post-checkout), edytor szablonów, globalny log /emails z filtrami, „Wyślij ponownie" jednym kliknięciem, sandbox mode dla demo.

Widgety web

Frontend

Kalendarz dostępności, kalkulator ceny, opinie gości, formularz zapytania — wszystkie jako iframe embed do Framera/dowolnej strony. Generator kodu HTML w adminie. ISR z cache 5 min, rate-limited API, CORS.

Finanse

KPI

Przychód roczny + tracking progu podatkowego (rozliczenie ryczałtem 8,5% / 12,5%), rejestr kosztów po kategoriach, listy wydatków, zestawienie do PIT, widok „unsettled" rezerwacji.

Cennik

Logic

Reguły cenowe z priorytetami: BASE, SEASONAL (zakres dat), DAY_OF_WEEK (weekendowa stawka), SPECIAL (święta/long-stay), minimum nights, czasowe overlapy. Używane w widgecie pricing + w kreatorze rezerwacji.

Kalendarz operacyjny

Ops

Miesięczny widok rezerwacji, drag-over detalami (gość, telefon, status), eksport iCal (publiczny feed dla Booking/Google Calendar), filtry po nieruchomości.

Sprzątanie

Ops

Kanban board (PENDING → IN_PROGRESS → COMPLETED → VERIFIED), automatyczne taski przy checkout, assignees, kolorowanie przy deadline.

Ankiety gości

CX

Post-stay survey (5 ocen: overall, cleanliness, communication, check-in, value + wouldRecommend + comments), email automation wysyłka po checkout, agregacja ocen i statystyki na /surveys.

Zapytania (inquiries)

Funnel

Formularz zapytania na zewnętrznej stronie → automatyczny INSERT do Inquiry → notyfikacja emailem do adminów → konwersja jedną akcją do rezerwacji (zachowanie estimatedPrice, guestName, dat).

Multi-tenant SaaS

Arch

Pełna izolacja między organizacjami via IDOR guards (wszystkie mutacje scoped przez organizationId w chainie). Custom domains per org, middleware routing, sandbox mode. Gotowe do commercialization.

Audit log

Compliance

Pełna historia zmian (CREATE/UPDATE/DELETE/STATUS_CHANGE/SIGN/SEND_EMAIL), diff old → new, kto i kiedy, filtry per entity/user/data. Ważne dla księgowości i regulacji RODO. Automatic PII redaction (PESEL/password nigdy w metadanych).

Self-service konto

UX

User może sam zmienić hasło w /settings/account (stare + nowe, bcrypt 12 rund, walidacja długości). Admin reset hasła też istnieje, ale user nie musi do niego iść gdy sam pamięta stare.

Podgląd

System w akcji

Pełny panel adminowy, kalendarz operacyjny, moduł umów z edytorem WYSIWYG, globalny log maili z akcją „wyślij ponownie", kokpit finansowy, embeddable widgety do strony Framer — wszystko działa z realnymi danymi.

Przegląd rezerwacji, KPI, nadchodzące meldunki

Dashboard

Przegląd rezerwacji, KPI, nadchodzące meldunki

Filtry po statusie, płatności, platformie. Widoczne różne statusy

Lista rezerwacji

Filtry po statusie, płatności, platformie. Widoczne różne statusy

Timeline, dane gościa, umowy, email log, komentarze, audit history

Szczegóły rezerwacji

Timeline, dane gościa, umowy, email log, komentarze, audit history

Miesięczny widok, drag-over detalami, eksport iCal

Kalendarz operacyjny

Miesięczny widok, drag-over detalami, eksport iCal

Status badges, inline download podpisanego PDF

Lista umów

Status badges, inline download podpisanego PDF

Tiptap WYSIWYG, status stepper, sidebar z share linkiem i uploaded PDF

Umowa — edytor + workflow

Tiptap WYSIWYG, status stepper, sidebar z share linkiem i uploaded PDF

Filtry po statusie, okresie, gościu. Akcja „Wyślij ponownie" na wpisach FAILED

Globalny log emaili

Filtry po statusie, okresie, gościu. Akcja „Wyślij ponownie" na wpisach FAILED

Szablony i tryby wysyłki (auto/manual/disabled) dla 5 triggerów

Automatyzacja email

Szablony i tryby wysyłki (auto/manual/disabled) dla 5 triggerów

Agregacja ocen, rekomendacje, recenzje po pobycie

Ankiety gości

Agregacja ocen, rekomendacje, recenzje po pobycie

Przychód roczny, próg ryczałtu, koszty per kategoria

Finanse

Przychód roczny, próg ryczałtu, koszty per kategoria

Base / Seasonal / Day-of-week / Special z priorytetami

Cennik — reguły

Base / Seasonal / Day-of-week / Special z priorytetami

Generator iframe do osadzenia na zewnętrznej stronie

Widgety embed

Generator iframe do osadzenia na zewnętrznej stronie

Widget osadzony na domekwisniew.pl — wybór dat, obliczanie ceny, zapytanie

Kalendarz publiczny

Widget osadzony na domekwisniew.pl — wybór dat, obliczanie ceny, zapytanie

Bezpieczeństwo (priorytet #1)

Sprint pod kątem security — 6 znalezionych, 6 naprawionych

Przy rodzinnych danych nie ma kompromisów. Przeprowadziłem pełny audyt z diagnozą i fiksami, pokryty testami jednostkowymi.

critical

IDOR na mutacjach umów

updateContract / deleteContract / updateContractStatus nie weryfikowały czy umowa należy do org callera — user z org A mógł modyfikować umowy z org B znając tylko ID. Typowy IDOR bug.

Fix: contractBelongsToOrg() helper scoping przez relację Contract → Reservation → Property → organizationId. 7 nowych testów.

critical

XSS na stronie /share/[token]

dangerouslySetInnerHTML na contract.content bez sanityzacji. Admin (lub compromised account) mógłby wstrzyknąć <script> który odpalił się w przeglądarce gościa.

Fix: sanitize-html z whitelist Tiptap tagów, rel="noopener noreferrer nofollow" na linkach, 19 testów pokrywających payloady (onerror, javascript: URL, iframe, etc).

high

Trailing newline w env var

NEXT_PUBLIC_GUEST_FORM_URL w Vercelu miał \n na końcu (wpisany z Enterem). Copy-link do formularza produkował złamany URL: "https://panel.domekwisniew.pl\n/formularz/TOKEN".

Fix: lib/urls.ts z getBaseUrl()/getClientBaseUrl() — trim + strip trailing slashes, fallback chain. 17 testów regresji.

high

Brak rate-limitingu na API cennikowym

Konkurencja mogłaby enumerować całą siatkę cenową przez /api/public/calculate-price (ceny per noc dla każdej daty). Zero kosztów dla atakującego, business-intelligence leak.

Fix: getClientIp() + isRateLimited() na /availability (120/min), /calculate-price (60/min), /inquiry (5/15min), /ical (60/min).

medium

Token replay po użyciu

guestFormToken i surveyToken pozostawały ważne po pierwszym submissionie. Ktoś z leaked link (zrzut, forward email) mógł zobaczyć dane.

Fix: Token set to null w tej samej transakcji co save response. One-shot usage guaranteed.

medium

PII w audit logu

Jeśli diffChanges() byłby użyty na rezerwacji z PESEL/passwordHash, wartości trafiłyby plaintext do JSON AuditLog.changes.

Fix: SENSITIVE_FIELDS whitelist w lib/audit.ts. Redakcja *ALL* PII keys do [REDACTED] przed zapisem.

Ship-to-prod

Realne incydenty produkcyjne, fixed in hours

Tu się dzieje najciekawsza robota — ship-first oznacza też radzenie sobie z problemami które wychodzą dopiero na proda. W każdym z tych przypadków: diagnoza → test reprodukujący → fix → deploy, średnio w ciągu godziny.

Auto-wysyłka umowy padła, klientka nie dostała

~60 min

Symptom

Joanna Kałuska wypełniła formularz, guestFormFilledAt zapisane, ale 0 umów i 0 emailów w bazie. Status rezerwacji DRAFT.

Diagnoza

submitGuestForm wywoływał generateContractFromTemplate który robił requireAuth() → redirect(/login) rzuca wyjątek → catch cichutko połyka → form zwraca success ale nic się nie stało.

Fix

Extract _generateContractFromTemplate do shared internal fn (wzorowane na istniejącym sendContractEmailInternal). Public wrapper z auth nadal istnieje dla admin. 15 regression testów.

Vercel build crash po upgradzie Resend

~30 min

Symptom

Deploy failed z "Missing API key. Pass it to the constructor new Resend(re_123)". Action: każdy route importujący @/lib/email zabijał build.

Diagnoza

Resend SDK ≥6.0 rzuca w konstruktorze jeśli API key brak. Vercel build phase "collect page data" nie ma dostępu do env (secret-only-runtime), eager new Resend(undefined) w lib/email.ts.

Fix

Lazy proxy. Instancja tworzona przy pierwszym resend.emails.send(), nie przy module load. Zero zmiany dla callerów.

Upload umowy blokowany dla skanów

~20 min

Symptom

Klient wydrukował umowę, podpisał ręcznie, zeskanował i dostał 422 „Plik PDF nie zawiera podpisu cyfrowego, podpisz w mObywatel…".

Diagnoza

verifyPdfSignature() rzucał reject przy PDF bez podpisu PAdES. Blokada wynikała z zbyt rygorystycznej walidacji — rodzina Wiśniew głównie dostaje umowy skanowane.

Fix

Verification is now best-effort — try/catch, continue without. Jeśli podpis jest: zielony banner w emailu do właściciela. Jeśli nie: pomarańczowy „weryfikacja manualna".

Kalendarz blokował turnover days

~15 min

Symptom

Formularz zapytania blokował wybór daty wymeldowania gdy inny gość miał w tym dniu zameldowanie. Klient nie mógł wybrać sensownego terminu.

Diagnoza

isBooked(dateStr >= r.from && dateStr < r.to) traktował dzień zameldowania jako „zajęty", ale w logice wynajmu to turnover day — checkout rano, check-in popołudniu, legitne.

Fix

Dual check: isBooked dla start-date picker (strict), isBookedMidstay dla end-date picker (tylko strictly inside another stay). Calendar znów klika turnover days.

Narzędzia

Stack — dobór pod efekt, nie pod framework

AI co-pilot

Claude Code

Opus 4.7 w 1M-context window jako primary vibecoder, nie dodatek. Opisuję problem biznesowy po polsku, Claude proponuje architekturę, iterujemy, test, deploy. Każdy feature z tej strony powstał w takiej pętli.

Frontend / Backend

  • Next.js 14 App Router + Server Components
  • TypeScript strict mode
  • Prisma ORM + migracje
  • NextAuth v5 (credentials)
  • Tiptap WYSIWYG editor
  • sanitize-html XSS guard
  • Zod schema validation
  • Vitest (200 testów)

Infra / integracje

  • Vercel deploy + cron + domains
  • Neon Postgres serverless DB
  • Vercel Blob file storage (PDFs)
  • Resend transactional email
  • node-forge PAdES signature verify
  • iCal public calendar feed
  • Playwright screenshot automation
  • GitHub + auto-deploy na Vercel

Metryki

Liczby — 6 tygodni, 1 osoba + Claude

28klinii TS/TSX
150plików
200testów (zielonych)
37commitów
19tabel Prisma
5publicznych API
4webhook & cron
5rezerwacji produkcyjnie

Jak pracuję z AI

Claude jako pair-programmer, nie generator kodu

Pętla jest prosta: opisuję problem biznesowy po polsku („gość wypełnił formularz ale umowa nie przyszła"), Claude diagnozuje z bazy lub kodu, proponuje fix, ja review → testy → deploy.

Discovery po rollout. Zaczynam od mapowania flow biznesowego (kto, co, gdzie, kiedy), hipoteza, metryka sukcesu — potem dopiero stack i kod.

Ship nad clean code. Jak coś działa, idzie na proda. Testy i refactor jako reakcja na realne incydenty, nie prewencyjnie „enterprise".

Przykład z produkcji

User (ja)

sprawdź w bazie czy została wysłana umowa do klientki Joanna Kałuska

Claude (po kilku tool calls)

Rezerwacja istnieje, contractTemplateId ustawiony,
guestFormFilledAt = 15:31,
ALE: 0 umów, 0 emaili.

Status umowy: DRAFT. Fix: extract `generateContractFromTemplateInternal` — obecna wersja wywołuje `requireAuth()` w publicznym flow, rzuca redirect, catch cicho połyka.

Od zauważenia problemu do deployu fixa: ~60 minut, z testami reprodukującymi błąd.

Demo

Zaloguj się i poklikaj

Sandbox — fake data

Demo organizacja z 10 rezerwacjami, umowami, ankietami. Dane są zupełnie fikcyjne, izolowane od produkcji. Emaile są tylko logowane — Resend nigdy nie zostanie wywołane. Reset danych co 24h (Vercel Cron).

Emaildemo@wisniew.pl
Hasłodemo123
→ Otwórz demo

Co warto sprawdzić

  1. Dashboard z przeglądem rezerwacji
  2. Lista rezerwacji — status badges, timeline, akcje
  3. Szczegóły umowy z inline podglądem PDF
  4. /emails — globalny log z filtrami + „Wyślij ponownie"
  5. Ankiety — agregacja ocen, recenzje
  6. Finanse — przychód roczny, próg podatkowy
  7. Cennik — reguły cenowe (base/sezonowe/weekendowe)
  8. Widgety embed — generator iframe do Framera
  9. /audit — pełna historia zmian (sandbox: tylko kilka wpisów)

Demo user ma rolę OPERATOR — widzi ~95% funkcji, bez dostępu do zarządzania użytkownikami i billingu.

Kontakt

Porozmawiajmy

Ten case study istnieje głównie pod ofertę AI VibeCoder for business w Locon. Jeśli Ty, rekruterze, to czytasz — wszystko czego szukasz (ship-fast, LLM-driven, business-first) znajdziesz tu w formie działającego produktu, nie slajdów.

Chętnie pokażę co jeszcze — od pracy z AI agentami i RAG, przez workflow automation (call center, lead scoring, marketing automation), po KPI dashboards i integracje.

Dane

Jakub Kazimierczyk

AI VibeCoder · Product builder