/pl/ case-studies / More Than Control Tower
// Case study · Produkt / Supply Chain Platform

More Than Control Tower — modułowa platforma dla firm produkcji i dystrybucji towarów

Gotowy produkt do zarządzania łańcuchem dostaw — dopasowywany pod konkretny zakład klienta. 8 bounded contexts (magazyn, sprzedaż, spedycja, CRM, HR, księgowość, system, auth) zaprojektowanych jako plug-and-play — każdy działa standalone albo podłącza się do istniejącego u klienta systemu (Fakturownia, zewnętrzny WMS, ERP, HR). 26 udokumentowanych decyzji architektonicznych, 600+ testów, stack ESM-native: TypeScript 6 + NestJS 11 + PostgreSQL 18.

Klient · Adaptowane per-wdrożenie
Forma · Produkt + customizacja
Rola · Pełna własność produktu
Sektor · Supply Chain / B2B
8
Bounded contexts
26
Udokumentowanych decyzji (ADR)
600+
Testów (unit + BDD + integration)
0
Vendor lock-inów
01 · Kontekst

Produkt i jego kontekst rynkowy#

Typowy klient w branży produkcyjno-dystrybucyjnej już używa Fakturowni do faktur, ma własny WMS od dostawcy regałów, importuje CSV z TMS przewoźnika, prowadzi HR w Excelu. Brakuje warstwy, która to wszystko spina i daje jeden widok operacyjny — kto co produkuje, gdzie to leży, dokąd to jedzie, kto za to zapłacił. MTCT to ta warstwa. Z możliwością pełnego natywnego stacku dla firm, które dopiero zaczynają.

Target
SMB produkcja + dystrybucja
firmy które produkują i dostarczają towar
Forma kontraktu
Produkt + customizacja per-projekt
Model wdrożenia
On-prem lub managed · single-tenant
Modularność
8 bounded contexts · każdy swappable
Własność
philosopht — pełna kontrola roadmapy
Status produktu
Rdzeń gotowy · adaptery per-klient
Cel produktu

Dostarczyć modułową platformę B2B do zarządzania łańcuchem dostaw — taką, w której każdy moduł da się odłączyć i podłączyć do istniejącego u klienta systemu. Zamiast wymuszać "rip and replace", MTCT spina to, co klient już ma — Fakturownię do faktur, zewnętrzny WMS, ERP, HR — i wypełnia luki natywnymi modułami tam, gdzie klient ich nie ma. Architektura: hexagonal + CQRS + Domain Events, każda integracja zewnętrzna za portem, każdy moduł testowany na trzech poziomach (unit, BDD, integration z Testcontainers).

02 · Wyzwania

Co musieliśmy rozwiązać#

Platforma multi-modularnego rodzaju ma sens tylko wtedy, kiedy moduły są naprawdę wymienne — a nie wtedy, kiedy "mówimy, że są". To wymagało wcześnie podjętych, dyscyplinujących decyzji architektonicznych. Każda z dwunastu pozycji niżej to konkretne wyzwanie, na które musieliśmy mieć odpowiedź zanim napisaliśmy pierwszy controller.

Plug-and-play moduły

Każdy z 8 bounded contexts musi działać standalone albo podłączać się do zewnętrznego systemu klienta. Bez magii — porty i behavioral contracts.

Komunikacja bez bezpośrednich importów

Moduły nigdy nie importują się wzajemnie. Rozmawiają przez QueryBus (read), CommandBus (write) i Domain Events (reaktywność). Granice są wymuszone, nie umowne.

Modular monolith z planem na microservices

CQRS busy są warstwą transportu. Migracja do microservices = podmiana in-memory bus na HTTP/gRPC lub RabbitMQ. Bez przepisywania domeny.

Spójność danych

ACID wewnątrz agregatu (jeden moduł, jedna transakcja). Eventual consistency między modułami — Domain Events propagują zmiany asynchronicznie.

Pełna szczelność typów end-to-end

OpenAPI → Kubb → TanStack Query hooks. Zmiana w NestJS controllerze to compile-error w React, nie cichy runtime crash.

Domain Driven Design w praktyce

Agregaty z metodami, value objects (Money, GoodDimensions), domain errors. DDD jako rygor projektowy, nie tylko nazwa folderu.

Testowanie na trzech poziomach

Unit (domain logic, ~513 testów) + BDD handlers (CQRS z in-memory repos) + integration (Testcontainers z prawdziwym PostgreSQL).

Autoryzacja jako port

Moduły nie wiedzą o rolach. Pytają AuthorizationPort.canDo(action, user). Migracja na Keycloak / Auth0 = wymiana jednej implementacji.

Integracje z systemami klienta

Fakturownia.pl, zewnętrzny WMS, ERP, HR, TMS — każda integracja za portem z behavioral contractem. Adapter piszemy gdy znamy realne API.

CLI dla operatorów

nest-commander dzieli CQRS busy z HTTP — jedna logika, dwa punkty wejścia. Operator robi przez terminal to samo, co użytkownik przez UI.

ESM-native TypeScript 6

Cały stack ESM, nodenext resolution, brak CJS-only zależności. Stack przygotowany na kolejne 5 lat zamiast dług technologiczny od pierwszego dnia.

Multi-language UI od pierwszego dnia

Inlang Paraglide — kompilowane tłumaczenia, type-safe klucze, zero runtime overhead. PL i EN są obywatelami pierwszej kategorii, nie afterthought.

"
Testy nie są opcjonalnym work-follow-upem — są częścią feature'a. Bez nich nie ma "gotowe".
P
philosopht
Reguła inżynierska MTCT

Macie podobnie złożony projekt? Pogadajmy.

Mapy, role, integracje z innymi zespołami i skala — wiemy, jak to ułożyć w spójny produkt mobilny.

03 · Podejście

Jak budujemy rdzeń produktu #

Trzy etapy, w których powstaje moduł MTCT. Ten sam wzorzec dla wszystkich ośmiu bounded contexts — przewidywalność jest cechą produktu, nie biurokracją.

01

Bounded context i kontrakty

Wydzielamy moduł z własnym językiem domeny, własnymi portami, własnymi tabelami (prefix `warehouse_`, `sales_`, …). Decydujemy, co publikujemy jako Domain Event, a co zostaje wewnętrzne. Brak fizycznych foreign keys między modułami — logiczne referencje (UUID).

02

Domain-first implementation

Agregaty z metodami, command handlers, query handlers, domain events. Controllers to cienka warstwa HTTP, ORM to detal infrastruktury. Każdy test jest pisany razem z featurem — nigdy po.

03

Behavioral contracts dla adapterów

Zamiast spekulatywnych adapterów do "jakichś" zewnętrznych systemów — definiujemy port i zestaw scenariuszy "co adapter musi umieć". Pierwszy klient = pierwszy adapter, ale interfejs i testy gotowe.

Pełna własność techniczna

Stack open source, ESM-native: PostgreSQL, NestJS, MikroORM, Vitest. Zero vendor lock-inów. Kod gotowy do audytu, do przekazania zespołowi klienta, do migracji na własną infrastrukturę. Architektura przygotowana na ekstrakcję do microservices bez przepisywania domeny.

04 · Ekrany

Jak wygląda produkt w praktyce#

Dziewięć widoków z referencyjnej instancji MTCT — od centrum operacyjnego po panel konektorów. W realnym wdrożeniu układ, kolor i zakres modułów dopasowujemy do brandingu i potrzeb klienta.

Ekrany pochodzą z referencyjnej instancji MTCT z seedowanymi danymi demo. W realnym wdrożeniu kolor, układ i zakres modułów dopasowujemy do brandingu i potrzeb klienta.

05 · Wyniki

Co jest w pudełku#

Liczby z aktualnej wersji rdzenia MTCT. Każda z nich to konsekwencja konkretnej decyzji architektonicznej, nie efekt uboczny.

8
bounded contexts (magazyn, sprzedaż, spedycja, CRM, ERP, HR, system, auth)
26
udokumentowanych ADR — każdy z konsekwencjami i alternatywami
600+
testów łącznie (~513 unit + BDD handlers + integration z Testcontainers)
100%
type-safe od OpenAPI po React TanStack Query hooks (Kubb)
0
vendor lock-inów — stack open source, każdy moduł swappable
3.8×
Vitest szybszy od Jest — pomiar z migracji w ADR-011
Macie podobny projekt na horyzoncie?Zobacz, jak prowadziliśmy inne złożone wdrożenia z wieloma użytkownikami i rolami.
Inne realizacje
Od tego miejsca — technical deep dive
Niżej — architektura, stack i środowiska: trzy decyzje techniczne, które najmocniej kształtują produkt, plus pełny rozkład bibliotek i ścieżek wdrożenia.
06 · Architektura i podejście techniczne

Trzy decyzje, które definiują produkt#

Plug-and-play nie bierze się z marketingu. Bierze się z trzech konkretnych decyzji architektonicznych, podjętych zanim napisaliśmy pierwszy moduł. Każda ma swój ADR — z uzasadnieniem, konsekwencjami i odrzuconymi alternatywami.

Decyzja 01 · Komunikacja między modułami

CQRS Bus jako jedyny kontrakt między bounded contexts

Modułowy monolit, w którym moduły importują się wzajemnie, to monolit udający coś więcej. Pierwszy lazy import łamie izolację, drugi tworzy cykliczną zależność, trzeci sprawia, że "wymienialny moduł" staje się fikcją.

W MTCT moduły nigdy nie importują się wzajemnie. Rozmawiają wyłącznie przez trzy magistrale: QueryBus (read między modułami — np. spedycja pyta CRM o godziny dostępności klienta), CommandBus (write — wymuszenie zmiany w innym module), EventBus (reaktywność — sprzedaż publikuje OrderPlaced, magazyn rezerwuje towar, spedycja dodaje do planowania).

Konsekwencja: migracja na microservices to wymiana implementacji magistrali, nie przepisywanie domeny. In-memory bus → HTTP/gRPC dla read, RabbitMQ/Kafka dla eventów. Domena, handlery, agregaty zostają bez zmian. To nie obietnica — to architektoniczna gwarancja, wymuszona zakazem direct imports.

Decyzja 02 · Adaptery do zewnętrznych systemów

Standalone-first — adapter piszemy gdy znamy realne API

Frameworki integracyjne kuszą "uniwersalnymi adapterami" do popularnych systemów — WMS, ERP, fakturowni. Brzmi pragmatycznie, ale w praktyce: każda taka integracja oparta jest na zgadywaniu, jak API "powinno" wyglądać. Pierwszy realny klient z systemem v3 zamiast v4 = przepisanie od zera.

W MTCT każdy zewnętrzny system jest za portem (np. InvoicePort, WarehouseSyncPort). Zanim mamy klienta, definiujemy tylko interfejs i behavioral test contract — zestaw scenariuszy "co adapter musi umieć". Sam adapter piszemy dopiero, kiedy znamy konkretne API klienta.

Konsekwencja: zero spekulatywnego kodu, zero martwych integracji. Rdzeń jest gotowy, adaptery dopisujemy per-wdrożenie — i każdy z nich od pierwszego dnia jest precyzyjnym dopasowaniem do realnego API, nie kompromisem.

Decyzja 03 · Autoryzacja i tożsamość

Autoryzacja jako port — moduły nie wiedzą o rolach

Klienci mają różne wymagania dotyczące autoryzacji. Mały zakład wystarczy mieć rolami zaszytymi w bazie. Średnia firma chce LDAP. Korporacja wymaga SSO przez Keycloak albo Azure AD. Jeśli logika autoryzacji jest rozsiana po modułach, każdy z tych wariantów to przepisanie ośmiu bounded contexts.

W MTCT moduły nigdy nie sprawdzają ról bezpośrednio. Pytają AuthorizationPort.canDo(action, user, context). Co więcej — moduł System (tożsamość: email, imię, role) jest fizycznie oddzielony od modułu Auth (credentials: passwordHash, refresh tokens). To pozwala wymienić jedno bez ruszania drugiego.

Konsekwencja: migracja z lokalnego RBAC na Keycloak = napisanie jednego adaptera KeycloakAuthorization. Reszta produktu nie wie, że coś się zmieniło. To samo dla Auth0, własnego OIDC, czy dowolnego nowego rozwiązania, które wymyśli klient.

Tech stack#

Wybór bibliotek wokół trzech decyzji powyżej. Standardowe, dobrze utrzymywane, open source, ESM-native. Bez ryzyka osierocenia, bez vendor lock-inów.

JĘZYK I RUNTIME

TypeScript 6Node.js 22ESM nativenodenext resolution

FRAMEWORK BACKEND

NestJS 11@nestjs/cqrs@nestjs/event-emitternest-commander

ORM I BAZA

MikroORM 7PostgreSQL 18schema-first defineEntitymigrations CLI

WALIDACJA

Zod 4class-validatorruntime + compile-time

TESTOWANIE

Vitest 4@testcontainers/postgresqlsupertestBDD handlers

AUTH I BEZPIECZEŃSTWO

JWT httpOnly cookiesArgon2Authorization portrefresh token rotation

FRONTEND

ViteReactTanStack QueryTanStack FormInlang ParaglideKy

API I KLIENT

OpenAPI · SwaggerKubb (codegen)TanStack Query hooksshared-types pkg

Środowiska i ścieżki wdrożenia#

Cztery środowiska — od lokalnego dev po single-tenant produkcyjny u klienta. Im bliżej klienta, tym sztywniejsze kryteria i większy udział customizacji.

// release.pipeline target: prod
// release.pipeline 01 · Dev Deweloperskie → dev client→ hot reload→ mockowane dane // odbiorcy zespół philosopht 02 · Pre-test Pre-testerskie → świeży snapshot API→ smoke test wewn. // odbiorcy testerzy SmallGIS 03 · TestFlight Testerskie → TestFlight + Android→ test w terenie // odbiorcy myśliwi · koła PZŁ 04 · Prod Produkcyjne → środowisko docelowe→ release ← wszystkie etapy // odbiorcy wszyscy członkowie PZŁ coraz ostrzejsze kryteria wejścia →

Chcecie podobne wyniki u siebie?

Wstępna analiza i wycena w 48h. Bez zobowiązań — pierwszy krok zawsze po naszej stronie.

07 · Dev story

Plug-and-play moduły — port + trzy implementacje #

Problem inżynierski · Autoryzacja

Każdy moduł działa solo. Albo podłącza się do tego, co klient już ma.

Plug-and-play brzmi jak slogan. Pod spodem to bardzo konkretne ograniczenie: moduł nie może wiedzieć, kto dostarcza implementację. Nie może zakładać, że role są w bazie, nie może wołać konkretnego SDK, nie może instalować Keycloak-client jako runtime dependency.

Standardowa odpowiedź — "wszystko za interfejsem" — jest niewystarczająca. Interfejs bez behavioral contractu to obietnica bez egzekucji. Pierwszy adapter, który zwróci null zamiast pustej tablicy w nieprzewidzianym scenariuszu, łamie cały moduł — i nikt o tym nie wie do produkcji.

W MTCT obok każdego portu powstaje zestaw scenariuszy behawioralnych — testy, które każdy adapter musi przejść, niezależnie od backendu (fake, lokalne RBAC, Keycloak, Auth0). Dzięki temu wymiana implementacji jest operacją mechaniczną, nie research projektem. Port definiuje "co" — contract testy definiują "jak" — adapter dostarcza "czym".

// rdzeń: moduły rozmawiają z portem, nie z implementacją
interface AuthorizationPort {
  canDo(action: Action, user: UserCtx): Promise<boolean>;
}

// trzy wymienne implementacje, jeden behavioral contract:
class FakeAuthorization      implements AuthorizationPort { /* dla testów */ }
class RoleBasedAuthorization implements AuthorizationPort { /* lokalne RBAC */ }
class KeycloakAuthorization  implements AuthorizationPort { /* klient z SSO */ }
"
Port definiuje "co". Contract test definiuje "jak". Adapter dostarcza "czym". Każdą z tych warstw można wymienić niezależnie.
P
philosopht
Zasada plug-and-play w MTCT
Spis treści
  1. 01 Kontekst projektu
  2. 02 Wyzwania
  3. 03 Podejście
  4. 04 Ekrany aplikacji
  5. 05 Wyniki
  6. 06 Architektura i tech
  7. 07 Dev story
  8. Kontakt
Gotowi na Twój kontakt

Powiedz, czego potrzebujesz.

Masz pomysł na aplikację lub potrzebujesz wsparcia technologicznego? Napisz do nas — przygotujemy wstępną analizę i wycenę w 48h.

Napisz do nas
[email protected]
Siedziba
philosopht Dawid Michota
ul. Świętokrzyska 41A
26-001 Wola Kopcowa, Polska
NIP 6573002241
Spotkajmy się
Możemy spotkać się stacjonarnie:
Kielce Warszawa Kraków Katowice Łódź Radom
Bezpłatna konsultacja