Skip to main content

bff-consumer-service

Bounded Context: BFF (Meta / Discovery) · Owner: Frontend Platform · Phase: 1 · Storage: No own write model. Memorystore (session + facet cache) + Cloud SQL Postgres (analytics outbox + signed-handoff replay log) · Bundle: services/bff-consumer-service/

bff-consumer-service is the Backend-for-Frontend for the consumer meta layer of Ghasi Melmastoon — the Trivago-like web app (Next.js) and the React Native consumer mobile app where anonymous travellers search across all tenants, compare hotels on list and map views, peek at the tenant brand, and hand off into the tenant-branded booking flow served by bff-tenant-booking-service.

It is a thin orchestration layer. It owns no domain state, no domain mutations, and no domain events. Every read it serves is composed from upstream services: search-aggregation-service for cross-tenant ranked listings and bounding-box map queries, pricing-service for cheapest-rate fanout (read-only quote previews), property-service for hotel detail surfaces, theme-config-service for the tenant brand peek shown on the detail card and the booking-handoff redirect. It owns only session ergonomics (guest session, search query state, recently viewed, wishlist), the signed handoff link, and conversion telemetry emitted as melmastoon.bff.consumer.* events.

The cloud is GCP. The desktop is Electron — but this BFF is never consumed by the desktop. It exists exclusively for the consumer web (@ghasi/app-web-meta) and consumer mobile (@ghasi/app-mobile-consumer).

Purpose

  • Be the single composition surface for the consumer meta layer, returning per-screen view-models that the web and mobile clients render verbatim — no client-side fanout to internal services, no cross-tenant joins on the client.
  • Enforce the anonymity boundary: the consumer surface never sees a tenant JWT, never reads from reservation-service / inventory-service / pricing-service directly (only through search-aggregation-service's cross-tenant projection or read-only quote previews), and never touches lock or payment internals.
  • Mint signed, single-use, HMAC-protected handoff links that hand a guest from the meta layer into the tenant booking surface served by bff-tenant-booking-service with pre-populated dates, occupancy, and currency.
  • Emit conversion-funnel telemetry (search.executed, click.recorded, handoff.initiated, wishlist.added, session.started/ended, locale.changed, currency.changed) so analytics-service can build attribution dashboards. Domain events are NEVER emitted from this BFF.
  • Apply aggressive cache and stampede protection so the marketing-campaign load profile (10× steady state for 30 minutes) does not flood search-aggregation-service or property-service.
  • Detect bot traffic (User-Agent, fingerprint, request cadence, behavioural signals) and apply per-bucket rate limits at the BFF before requests reach internal services.

Key responsibilities

  1. Composition: list view — accept (geo, dates, occupancy, filters, sortKey, locale, currency), fan out to search-aggregation-service (ranked, paginated), enrich top-N with cheapest-rate snapshot from pricing-service (read-only /quotes/preview), enrich brand peek from theme-config-service (logo + primary color), return a flat list-view model in ≤ p95 600 ms warm.
  2. Composition: map view — same query, bounding-box variant; returns up to 250 lightweight pins (no rate snapshot) plus 1 spotlighted pin with the rate snapshot for the user's current cursor target.
  3. Composition: hotel detail — fan out to property-service (rooms, amenities, photos, policies), search-aggregation-service (popularity + review summary), pricing-service (cheapest rate + 7-day price calendar preview), theme-config-service (brand peek). Compose into a detail view-model.
  4. Guest session — cookie-backed (gms_id) anonymous session in Memorystore (TTL 30 days). Carries localePreference, currencyPreference, recentlyViewed[] (capped 50), searchHistory[] (capped 25), wishlist[] (capped 100). Optional upgrade to authenticated session via iam-service (Phase 2+).
  5. Handoff mintingPOST /bff/consumer/v1/handoff/{tenantId}/{propertyId} constructs { sessionId, tenantId, propertyId, dates, occupancy, currency, locale, sourceCampaign?, expiresAt }, HMAC-signs it (HS256 over a per-environment rotating secret), and returns the signed redirect URL targeting https://{tenantSlug}.melmastoon.ghasi.io/book?h=<token>. Emits melmastoon.bff.consumer.handoff.initiated.v1.
  6. Wishlist — add / remove (cookie-keyed, no auth). Persists in Memorystore session blob and is replicated to a lightweight Postgres table (wishlist_anonymous) for cross-device merging when the guest later signs in.
  7. Telemetry emission — every meaningful interaction is emitted as a melmastoon.bff.consumer.* event into the platform Pub/Sub via the per-service outbox (Postgres outbox table, no domain row). Sampled at 100% for funnel events; 10% for search.executed; 100% for handoff.initiated.
  8. Bot mitigation — User-Agent allow-list with regex bot-detector, fingerprint hashing, request-rate buckets, CAPTCHA challenge handoff (Cloud reCAPTCHA Enterprise) on suspicious patterns, soft-deny via MELMASTOON.BFF.CONSUMER.SUSPECTED_BOT.
  9. Locale + currency — propagate Accept-Language to upstream services; resolve currency via tenant default (when known) or geo-IP (when not); never override an explicit user preference. Emit locale.changed.v1 / currency.changed.v1.
  10. Cache policysearch-aggregation-service query results in Memorystore (TTL 60 s, single-flight per (query-hash, currency, locale)); property-service detail in Memorystore (TTL 5 min); theme-config-service brand peek in Memorystore (TTL 15 min, invalidated on melmastoon.theme.published.v1).

Aggregates owned (session / projection only — no domain state)

AggregateCardinalityPurposeIdentity prefix
GuestSession1 per cookieAnonymous session blob in Memorystoregms_
SearchSession1 per active search queryLast query + filter context (TTL 1 h)srs_
RecentlyViewedchild of GuestSessionCapped 50, FIFO(composite)
Wishlistchild of GuestSession + Postgres mirrorCapped 100, dedup by propertyIdwsh_
BookingHandoffshort-lived, signedSingle-use signed handoff record (TTL 30 min)bhd_
MetaPageViewappend-only telemetry rowPage-view ledger for analytics outboxmpv_
ConversionFunnelEventappend-only telemetry rowFunnel-step ledger for analytics outboxcfe_
LocalePreferencesession fieldBCP-47 tag(n/a)
CurrencyPreferencesession fieldISO 4217 code(n/a)

There is no domain aggregate that the BFF owns transactionally. The only Postgres rows we persist are outbox, wishlist_anonymous, handoff_replay_log, bot_score_log, and the standard inbox / idempotency tables.

Key APIs (REST, /bff/consumer/v1)

MethodPathPurpose
POST/searchRanked list view (cross-tenant, paginated)
POST/search/mapBounding-box pins for map view
GET/hotels/{propertyId}Hotel detail view-model (composes 4 services)
GET/hotels/{propertyId}/availabilityLight availability snapshot for stay window
POST/wishlistAdd to wishlist (cookie-scoped)
DELETE/wishlist/{propertyId}Remove from wishlist
GET/wishlistRead current wishlist (with light enrichment)
POST/handoff/{tenantId}/{propertyId}Mint signed handoff link to tenant booking flow
POST/session/localeSet locale preference
POST/session/currencySet currency preference
GET/sessionRead current session (sanitized)
GET/facetsStatic + dynamic facet catalog (amenity types, geo cells)
POST/telemetry/page-viewClient-emitted page view (validated, persisted, fanned to outbox)
POST/telemetry/clickClick event for funnel analytics

All routes are anonymous-by-default. None require a JWT. Rate limits per docs/05-api-design.md §13. Hot-path endpoints sit behind Cloud CDN with Vary: Accept-Language, Accept-Encoding, X-Currency and short edge TTL (15 s) for /search warm queries.

Key events published

EventTriggerSample rate
melmastoon.bff.consumer.session.started.v1First request that mints gms_ cookie100%
melmastoon.bff.consumer.session.ended.v1Explicit logout / session TTL elapsed (best-effort)100%
melmastoon.bff.consumer.search.executed.v1/search or /search/map returned results10% (configurable)
melmastoon.bff.consumer.click.recorded.v1/telemetry/click accepted (listing → detail)100%
melmastoon.bff.consumer.handoff.initiated.v1Signed handoff token minted100%
melmastoon.bff.consumer.locale.changed.v1Locale changed via /session/locale100%
melmastoon.bff.consumer.currency.changed.v1Currency changed via /session/currency100%
melmastoon.bff.consumer.wishlist.added.v1Property added to wishlist100%
melmastoon.bff.consumer.wishlist.removed.v1Property removed from wishlist100%
melmastoon.bff.consumer.bot_suspected.v1Bot-detector tripped100%

All events carry the anonymous envelope: tenantId is null (cross-tenant by design); userId is null; sessionId is the gms_ ULID; requestId and traceId link to the originating request.

Key events consumed

The BFF reads upstream services synchronously via REST. It consumes no business events directly. The only Pub/Sub subscription it owns is for cache invalidation hints:

EventEffect
melmastoon.theme.published.v1Invalidate theme-config:peek:<tenantId> Memorystore key
melmastoon.search_aggregation.listing.indexed.v1Invalidate hot listing cache for the property + bust list-page cursor cache
melmastoon.tenant.suspended.v1Mark tenant slug as soft-blocked; /handoff returns MELMASTOON.BFF.CONSUMER.TENANT_SUSPENDED

Upstream / downstream

Upstream (we read): search-aggregation-service (ranked listings, map pins, facet catalog), property-service (hotel detail), pricing-service (read-only quotes/preview), theme-config-service (brand peek), tenant-service (slug → tenantId resolution, suspension status), iam-service (optional anonymous → authenticated upgrade in Phase 2).

Downstream (we publish for): analytics-service (funnel + conversion projection), audit-service (handoff mint trail), bff-tenant-booking-service (consumes the signed handoff token at /bff/tenant-booking/v1/bootstrap?h=<token>).

Non-functional requirements

NFRTarget
/search latency p95< 600 ms warm (cache hit on facets), < 1.5 s cold
/search latency p50< 180 ms warm
/hotels/{id} latency p95< 500 ms (4-way parallel fanout, single-flight)
/handoff latency p95< 120 ms (HMAC sign + outbox insert)
Availability99.9% monthly (degrades to read-only when search-aggregation-service slow; serves cached results with stale=true)
Cache hit ratio/search ≥ 60%; /hotels/{id} ≥ 80%; /facets ≥ 99%
Bot rejection accuracyFalse-positive < 0.5% on legitimate human traffic; > 90% catch on known bot signatures
Session storageMemorystore (Redis) — 1 GB working set, 30-day TTL
ReplicasMin 3 Cloud Run instances (Phase 1); auto-scale to 30 on campaign profile
Cost guardPer-tenant fanout budget (max 4 concurrent upstream calls per request); reject with MELMASTOON.BFF.CONSUMER.UPSTREAM_BUDGET_EXCEEDED

Where to go next