MARKETPLACE · BACKEND · INFRASTRUCTURE

Featherly

shipped

A two-sided tour marketplace for travelers, individual guides, and organizations, built around tenant-aware ownership, subscriptions, bookings, Stripe payments and payouts, and the operational infrastructure behind those flows.

Published November 14, 2024Updated September 25, 2026
Angular · NestJS · AWS · Stripe Connect · MongoDB
Featherly Historical Europe Tour detail page showing tour information, pricing, available dates, reviews, and owner controls.
FIG. 01From booking tours to running the marketplace behind them.

Featherly began with a familiar marketplace idea: guides publish tours, travelers discover them, and bookings connect the two. What made the project interesting was everything that had to become true behind that simple flow. A tour has an owner. An owner may be an individual or an organization. Subscription state affects what can be published. A booking moves through financial and operational states. Payments may need to reach a seller while the platform retains a fee, and the application still has to remain consistent when Stripe reports those changes asynchronously.

As those requirements accumulated, Featherly stopped behaving like a collection of marketplace screens and started behaving like a platform. The work expanded across the Angular client, NestJS API, tenant-aware authorization, subscription and payout flows, data modeling, caching, asynchronous processing, and AWS deployment. The challenge was no longer just building features; it was deciding where the rules behind those features should live so that ownership remained clear and financial state remained trustworthy as the product grew.

The marketplace underneath the booking flow

Featherly is a two-sided tour marketplace serving travelers on one side and individual guides or organizations on the other. Travelers can search and evaluate tours before moving into booking, while sellers need tools to publish inventory, manage bookings, operate under subscription limits, and receive payouts.

The public side is intentionally straightforward:

Public Featherly tour discovery page with destination search, price and difficulty filters, sorting, list and map controls, and tour cards
FIG. 02Public Featherly tour discovery page with destination search, price and difficulty filters, sorting, list and map controls, and tour cards

The discovery experience exposes the product in its simplest form. A traveler sees tours, filters the available inventory, compares price and difficulty, and opens a listing. That simplicity is important, but it hides several rules that the system has to enforce before a tour can appear there at all.

A listing is not merely content stored in a database. It has ownership, pricing, visibility, availability, and downstream booking consequences. Featherly supports Free, Pro, and Business subscription states, so publishing and visibility are connected to account entitlements. A tour associated with an individual guide also has different ownership context from one managed through an organization.

Search also extends beyond a conventional card grid. The same inventory can be explored spatially:

Immersive Featherly map search centered on Europe with search and sorting controls and a Historical Europe Tour marker in France
FIG. 03Immersive Featherly map search centered on Europe with search and sorting controls and a Historical Europe Tour marker in France

The map does not create a separate product model. It is another way of viewing the same tour inventory, which is one reason the underlying visibility rules matter. Whether a tour appears in a list, a search result, or on a map, the server still needs a consistent answer to the question: is this tour visible to this user in this context?

That is where the project moved beyond ordinary CRUD. The interesting part was not creating, reading, updating, and deleting a tour. It was making those operations obey the marketplace's business rules.

One product, two operating contexts

Once a user signs in, Featherly exposes a different side of the platform. The authenticated application combines tour discovery with operational navigation for bookings, owned tours, analytics, account settings, costs, billing, and payouts.

Authenticated Featherly dashboard showing navigation, tour search filters, and an owned Historical Europe Tour listing
FIG. 04Authenticated Featherly dashboard showing navigation, tour search filters, and an owned Historical Europe Tour listing

The same user can operate in a personal context or within an organization. That distinction affects which resources they may manage and which subscription rules apply, so I did not want tenant context to exist only as frontend state.

The frontend tracks the current context and sends it through an X-Tenant header. The backend then validates that context: the organization must exist, the user must actually be a member, and organization-scoped operations require the appropriate active Business subscription. This makes tenant selection part of the request contract rather than a visual switch the client is trusted to enforce.

The account-status flow follows the same principle. Instead of forcing multiple screens to independently reconstruct plan state, identity, entitlements, Stripe Connect state, and possible next actions, the backend aggregates those concerns into a unified account status response. That response can then drive guards and dashboard surfaces from one coherent interpretation of the account.

The result is a cleaner division of responsibility. The frontend decides how account state should be presented; the backend remains responsible for deciding what that state actually permits.

System architecture

Featherly is organized as an Nx monorepo with two main applications: an Angular 21 single-page application in apps/client and a NestJS 11 API in apps/server. They communicate over REST.

The client is built for Amazon S3 and distributed through CloudFront. The server can run as a conventional application during development but is packaged for AWS Lambda using Docker and Amazon ECR for deployment.

Diagram

Platform Services

Angular Frontend

NestJS Modular Monolith

Auth

Tour

Booking

Stripe / Connect

Organization

Account Status

Cache + Metrics + Health + Jobs + Upload

Traveler

Guide / Organization

Public discovery pages

Dashboard + tenant-aware tools

MongoDB

Redis cache / throttling

QStash jobs

S3 uploads

AWS Lambda + ECR

Stripe

The backend is intentionally a modular monolith. Domains such as authentication, tours, bookings, Stripe, organizations, users, reviews, and account state remain separated, while infrastructure concerns such as caching, metrics, health checks, uploads, and job dispatch have their own boundaries as well.

I preferred that shape to splitting the application into microservices prematurely. Featherly has enough domain complexity to benefit from explicit modules, but those domains are still evolving together. Keeping them in one deployable backend preserves straightforward transactions and development workflows without flattening the codebase into one indistinct service.

On the frontend, Angular provides the application shell, public and protected routes, shared UI, route guards, and stateful product flows. NGXS stores shared state including authentication, tours, bookings, tenant context, and account status. Application initialization also restores more than visual state: theme setup, error tracking, store initialization, storage rehydration, and authenticated-session restoration are part of startup, with an APP_INITIALIZER blocking initial application setup while the cookie-backed session is restored.

The backend owns the stronger guarantees. Controllers remain thin and delegate to services, while domain modules contain their schemas, DTOs, services, and routes. Cross-cutting behavior such as validation, throttling, optional authentication parsing, metrics capture, structured request IDs, security headers, compression, CORS, and exception handling is handled globally rather than duplicated inside feature code.

That boundary became increasingly valuable as the marketplace grew. The frontend can remain expressive without becoming the source of truth for business permissions, and the server can enforce rules without dictating every presentation detail.

Booking is where the simple product becomes a distributed workflow

A booking appears simple from a traveler's perspective: choose a tour, create the booking, pay, and receive confirmation. Once payment state crosses into Stripe, however, the application has to reason about more than one system.

Diagram
MongoDBQStash / Async jobsStripeNestJS APIAngular AppUserMongoDBQStash / Async jobsStripeNestJS APIAngular AppUserCreate booking and begin checkoutPOST /bookingsCreate pending bookingCreate payment intentpaymentIntents.create()Confirm paymentWebhook eventQueue or process eventMark booking paid / sync subscription state

Bookings are created with idempotency keys. Payment intents can route money to a seller's Stripe account while applying a platform fee. Payment completion is then confirmed asynchronously through webhook processing rather than by assuming the browser's checkout flow is the final source of truth.

That distinction matters because the user-facing action and the financial outcome are not the same event. A browser can close, a response can fail, or the provider can retry delivery. The application therefore needs its own durable interpretation of the payment and booking states rather than tying correctness to one HTTP request.

Webhook processing also participates in subscription state and downgrade behavior. This means Stripe integration is not isolated behind a checkout button; billing state influences what the application permits elsewhere.

Billing and payouts are product state, not a settings page

The Billing & Payouts surface makes those relationships visible.

Featherly Billing & Payouts page showing an active Pro subscription, a paused organization, and separate organization and personal payout states
FIG. 05Featherly Billing & Payouts page showing an active Pro subscription, a paused organization, and separate organization and personal payout states

A user's subscription, organization status, and payout destination affect what they can do across the rest of the application. In the interface shown above, personal plan state and organization state are presented separately, and payout configuration distinguishes between organization payouts and a personal Stripe Connect account.

This was an important modeling decision. A guide and an organization are not interchangeable simply because both can sell tours. The marketplace has to know which party owns the inventory and which account receives the corresponding funds.

Subscription changes have consequences as well. Plan limits affect tour publishing, and downgrade behavior can unpublish tours that no longer fit within the permitted plan. Existing bookings remain a separate concern from whether new inventory may remain publicly visible. Treating those rules as backend product logic keeps them from becoming scattered UI conditions that different screens can interpret differently.

Tour details combine discovery, ownership, and inventory

The tour detail experience is where many of those concerns meet. It has to work as a traveler-facing product page while also exposing appropriate management actions to the tour owner.

Historical Europe Tour detail page showing duration, group size, difficulty, discounted pricing, available start dates, reviews, and owner management actions
FIG. 06Historical Europe Tour detail page showing duration, group size, difficulty, discounted pricing, available start dates, reviews, and owner management actions

The visible information includes duration, group size, difficulty, price, rating, available dates, and reviews. The same page recognizes the authenticated owner's relationship to the tour and presents edit and delete actions while preventing the owner from booking their own listing.

That is another example of why resource ownership cannot be an afterthought. The route may be the same, but the permitted actions depend on who is looking at the resource and in which context.

The itinerary turns the tour from a generic product listing into structured travel inventory:

Historical Europe Tour itinerary showing scheduled stops on days 1, 3, 5, and 10 with a photo gallery below
FIG. 07Historical Europe Tour itinerary showing scheduled stops on days 1, 3, 5, and 10 with a photo gallery below

A tour can describe distinct stops and locations rather than relying on one large text description. Those stops then connect naturally to the geographic representation of the tour:

Historical Europe Tour gallery and route map with itinerary markers placed across several European destinations
FIG. 08Historical Europe Tour gallery and route map with itinerary markers placed across several European destinations

The route map and itinerary are two views of the same underlying product data. The itinerary gives the traveler a readable sequence; the map gives those stops spatial context. Keeping those representations tied to the tour model avoids maintaining separate, manually synchronized versions of the route.

These details matter to the case study because they show why the domain model grew. A "tour" eventually has to carry enough structure to support discovery, filtering, dates, pricing, ownership, reviews, an itinerary, geographical presentation, bookings, and downstream payment behavior without turning into a collection of unrelated special cases.

Business rules belong below the presentation layer

One of the strongest architectural lessons from Featherly was that visibility and ownership are business rules, not presentation details.

Public search applies a visibility filter connected to paid-plan and owner context. Tour creation enforces plan-based limits. Tenant context changes which resources a user can act on. Account state determines whether organization features are available. Booking logic prevents invalid actions such as booking one's own tour.

If those rules lived only in Angular components, they would be easy to bypass and difficult to keep consistent. If every service reimplemented them independently, the backend would gradually accumulate conflicting definitions of the same product policy.

Instead, I tried to keep the authoritative decisions at the backend boundaries and let the frontend consume the result. This is one of the reasons the account-status aggregation and tenant guard became more important over time: they allowed multiple product surfaces to ask the same underlying system for the same answer.

There is a line I kept coming back to while building Featherly:

Architecture is how a product keeps its promises.

For this project, those promises include not showing inventory that should be unavailable, not allowing one tenant to act on another tenant's resources, not granting features outside the active plan, and not allowing payment state to drift casually away from booking state.

Reliability had to extend beyond feature code

The deployment and operations story became part of the architecture as well. The Angular client is delivered through S3 and CloudFront, while the NestJS server is bundled for AWS Lambda and packaged through Docker and ECR. GitHub Actions supports the delivery workflow.

At runtime, Featherly also includes health and readiness endpoints, Prometheus metrics, structured logging with request-ID propagation, Sentry integration, Redis-backed caching and throttling, and QStash for asynchronous job dispatch.

None of those pieces is the feature a traveler signs up to use, but they support the features that matter. A booking workflow is difficult to trust if failed jobs disappear. Tenant-aware authorization is harder to operate if requests cannot be traced. A production API is harder to diagnose if it can only tell you whether the process is running, not whether its dependencies are ready.

Redis serves more than one purpose in that operational layer. It supports caching where repeated reads benefit from it and coordination for throttling, while the application can move asynchronous work out of the immediate request lifecycle through QStash.

The goal was not to add infrastructure because infrastructure looks impressive in a diagram. It was to introduce it where the product had a reason to depend on the guarantee it provided.

Why I kept the backend as a modular monolith

Featherly has many boundaries that could eventually be drawn as independent services: payments, bookings, accounts, organizations, tours, or asynchronous processing. I deliberately did not make that leap.

At this stage, separating every domain into a network service would introduce its own complexity: more deployment units, more inter-service failure boundaries, more distributed tracing requirements, and harder cross-domain changes. Those costs would arrive immediately even though the product's domains are still closely related.

The modular monolith gave me a more useful middle ground. Tour logic does not need to live inside Stripe logic, and organization rules do not need to leak into every controller, but those modules can still evolve inside the same application and deployment model.

That decision also made the codebase more honest. The system has meaningful internal boundaries without pretending it already needs the operational architecture of a much larger distributed platform.

What the project taught me

The biggest lesson was that product complexity rarely announces itself. It accumulates.

A search page becomes a visibility system once plans and ownership determine what is searchable. A guide profile becomes an account model once subscriptions and payouts depend on it. A checkout flow becomes a reconciliation problem once external payment state can change asynchronously. A dashboard becomes tenant-aware account orchestration once one person can operate personally and on behalf of an organization.

If the architecture never changes while the product changes, the code eventually starts lying about what the system actually is.

Featherly taught me to notice that transition earlier. It also changed how I think about operational work. Metrics, throttling, request IDs, health checks, caching, asynchronous jobs, webhook handling, and environment validation are not secondary concerns once the application handles bookings, subscriptions, and payouts. They are part of the reliability of the product itself.

The other lesson was to value architecture that is pragmatic without being careless. Featherly is not a purity exercise, and I did not want it to become one. A modular monolith was enough. REST was enough. Shared infrastructure was useful where the product needed it. Stronger boundaries were introduced where ownership, money, or recoverability made them worth the complexity.

That balance matters more to me than maximizing the number of technologies in the diagram.

Takeaway

Featherly started with a conventional product idea: give guides a place to publish tours and travelers a place to book them. What made it valuable as an engineering project was everything that happened after that first version of the idea met real product rules.

The platform now has to reason about individual and organization ownership, subscription entitlements, tour visibility, bookings, payment state, Stripe Connect payouts, asynchronous webhooks, tenant-aware authorization, caching, operational health, and deployment infrastructure. Those pieces are useful individually, but the more important work was making them agree with one another.

That is why I think of Featherly less as a collection of marketplace features and more as a lesson in designing systems around the promises a product makes.

A project can prove that you can build a feature. A stronger project shows that you can recognize when those features have become a system—and change the architecture accordingly.