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-servicedirectly (only throughsearch-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-servicewith 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) soanalytics-servicecan 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-serviceorproperty-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
- Composition: list view — accept
(geo, dates, occupancy, filters, sortKey, locale, currency), fan out tosearch-aggregation-service(ranked, paginated), enrich top-N with cheapest-rate snapshot frompricing-service(read-only/quotes/preview), enrich brand peek fromtheme-config-service(logo + primary color), return a flat list-view model in ≤ p95 600 ms warm. - 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.
- 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. - Guest session — cookie-backed (
gms_id) anonymous session in Memorystore (TTL 30 days). CarrieslocalePreference,currencyPreference,recentlyViewed[](capped 50),searchHistory[](capped 25),wishlist[](capped 100). Optional upgrade to authenticated session viaiam-service(Phase 2+). - Handoff minting —
POST /bff/consumer/v1/handoff/{tenantId}/{propertyId}constructs{ sessionId, tenantId, propertyId, dates, occupancy, currency, locale, sourceCampaign?, expiresAt }, HMAC-signs it (HS256over a per-environment rotating secret), and returns the signed redirect URL targetinghttps://{tenantSlug}.melmastoon.ghasi.io/book?h=<token>. Emitsmelmastoon.bff.consumer.handoff.initiated.v1. - 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. - Telemetry emission — every meaningful interaction is emitted as a
melmastoon.bff.consumer.*event into the platform Pub/Sub via the per-service outbox (Postgresoutboxtable, no domain row). Sampled at 100% for funnel events; 10% forsearch.executed; 100% forhandoff.initiated. - 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. - Locale + currency — propagate
Accept-Languageto upstream services; resolve currency via tenant default (when known) or geo-IP (when not); never override an explicit user preference. Emitlocale.changed.v1/currency.changed.v1. - Cache policy —
search-aggregation-servicequery results in Memorystore (TTL 60 s, single-flight per(query-hash, currency, locale));property-servicedetail in Memorystore (TTL 5 min);theme-config-servicebrand peek in Memorystore (TTL 15 min, invalidated onmelmastoon.theme.published.v1).
Aggregates owned (session / projection only — no domain state)
| Aggregate | Cardinality | Purpose | Identity prefix |
|---|---|---|---|
GuestSession | 1 per cookie | Anonymous session blob in Memorystore | gms_ |
SearchSession | 1 per active search query | Last query + filter context (TTL 1 h) | srs_ |
RecentlyViewed | child of GuestSession | Capped 50, FIFO | (composite) |
Wishlist | child of GuestSession + Postgres mirror | Capped 100, dedup by propertyId | wsh_ |
BookingHandoff | short-lived, signed | Single-use signed handoff record (TTL 30 min) | bhd_ |
MetaPageView | append-only telemetry row | Page-view ledger for analytics outbox | mpv_ |
ConversionFunnelEvent | append-only telemetry row | Funnel-step ledger for analytics outbox | cfe_ |
LocalePreference | session field | BCP-47 tag | (n/a) |
CurrencyPreference | session field | ISO 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)
| Method | Path | Purpose |
|---|---|---|
POST | /search | Ranked list view (cross-tenant, paginated) |
POST | /search/map | Bounding-box pins for map view |
GET | /hotels/{propertyId} | Hotel detail view-model (composes 4 services) |
GET | /hotels/{propertyId}/availability | Light availability snapshot for stay window |
POST | /wishlist | Add to wishlist (cookie-scoped) |
DELETE | /wishlist/{propertyId} | Remove from wishlist |
GET | /wishlist | Read current wishlist (with light enrichment) |
POST | /handoff/{tenantId}/{propertyId} | Mint signed handoff link to tenant booking flow |
POST | /session/locale | Set locale preference |
POST | /session/currency | Set currency preference |
GET | /session | Read current session (sanitized) |
GET | /facets | Static + dynamic facet catalog (amenity types, geo cells) |
POST | /telemetry/page-view | Client-emitted page view (validated, persisted, fanned to outbox) |
POST | /telemetry/click | Click 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
| Event | Trigger | Sample rate |
|---|---|---|
melmastoon.bff.consumer.session.started.v1 | First request that mints gms_ cookie | 100% |
melmastoon.bff.consumer.session.ended.v1 | Explicit logout / session TTL elapsed (best-effort) | 100% |
melmastoon.bff.consumer.search.executed.v1 | /search or /search/map returned results | 10% (configurable) |
melmastoon.bff.consumer.click.recorded.v1 | /telemetry/click accepted (listing → detail) | 100% |
melmastoon.bff.consumer.handoff.initiated.v1 | Signed handoff token minted | 100% |
melmastoon.bff.consumer.locale.changed.v1 | Locale changed via /session/locale | 100% |
melmastoon.bff.consumer.currency.changed.v1 | Currency changed via /session/currency | 100% |
melmastoon.bff.consumer.wishlist.added.v1 | Property added to wishlist | 100% |
melmastoon.bff.consumer.wishlist.removed.v1 | Property removed from wishlist | 100% |
melmastoon.bff.consumer.bot_suspected.v1 | Bot-detector tripped | 100% |
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:
| Event | Effect |
|---|---|
melmastoon.theme.published.v1 | Invalidate theme-config:peek:<tenantId> Memorystore key |
melmastoon.search_aggregation.listing.indexed.v1 | Invalidate hot listing cache for the property + bust list-page cursor cache |
melmastoon.tenant.suspended.v1 | Mark 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
| NFR | Target |
|---|---|
/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) |
| Availability | 99.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 accuracy | False-positive < 0.5% on legitimate human traffic; > 90% catch on known bot signatures |
| Session storage | Memorystore (Redis) — 1 GB working set, 30-day TTL |
| Replicas | Min 3 Cloud Run instances (Phase 1); auto-scale to 30 on campaign profile |
| Cost guard | Per-tenant fanout budget (max 4 concurrent upstream calls per request); reject with MELMASTOON.BFF.CONSUMER.UPSTREAM_BUDGET_EXCEEDED |
Where to go next
- Implementation-grade detail:
services/bff-consumer-service/SERVICE_OVERVIEW.mdand the rest of the 17-doc bundle. - Search projection contract (the only legitimate cross-tenant read path):
docs/03-microservices/search-aggregation-service.md. - BFF contracts in the cross-cutting API doc:
docs/05-api-design.md§9.1. - Handoff signing scheme and security posture:
docs/07-security-compliance-tenancy.mdandservices/bff-consumer-service/SECURITY_MODEL.md. - Funnel event taxonomy:
docs/04-event-driven-architecture.mdandservices/bff-consumer-service/EVENT_SCHEMAS.md.