EVENT_SCHEMAS — bff-consumer-service
Sibling: API_CONTRACTS · DATA_MODEL · DOMAIN_MODEL
Cross-cutting: 04 Event-Driven Architecture · Standards · NAMING
1. Scope and posture
This BFF emits only telemetry events under the melmastoon.bff.consumer.* subject prefix. It emits no domain events. It consumes a small set of platform events solely for cache invalidation. All emitted events carry the anonymous envelope — tenantId is null on every payload (because the consumer surface is cross-tenant), and the only stable identifier is guestSessionId.
| Aspect | Posture |
|---|---|
| Backbone | GCP Pub/Sub |
| Outbox | Postgres outbox table (per-service, drained by outbox-relay worker) |
| Delivery semantics | At-least-once; consumers must dedupe by eventId |
| Subject pattern | melmastoon.bff.consumer.<aggregate>.<verb-past-tense>.v<n> |
| Retention class | operational for session.*, search.*, click.*, wishlist.*, locale.*, currency.*; audit for handoff.initiated.v1, bot_suspected.v1 |
| Schema registry | Schemas live in @ghasi/event-envelope/schemas/bff-consumer/ and are validated in CI |
| PII posture | No raw IP, no raw UA, no email, no name. Only hashed identifiers and guestSessionId |
2. Event envelope (cross-cutting)
All events are wrapped in the platform envelope per 04:
{
"envelope": {
"eventId": "evt_01H8YN7QV4D8KZ4F8Y5CK4MV3D",
"subject": "melmastoon.bff.consumer.search.executed.v1",
"version": 1,
"occurredAt": "2026-04-23T09:14:22.041Z",
"publishedAt": "2026-04-23T09:14:22.058Z",
"producer": "bff-consumer-service",
"producerInstance": "bff-consumer-asia-south1-7f8d9c-x4z2",
"tenantId": null,
"userId": null,
"sessionId": "gms_01H8YN7Q2P7GZ4F8Y5CK4MV3DT",
"requestId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"causationId": null,
"correlationId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"schemaUri": "https://schemas.melmastoon.ghasi.io/bff-consumer/search-executed/v1.json",
"retentionClass": "operational",
"samplingRate": 0.1
},
"payload": { /* event-specific */ }
}
tenantId is always null on this BFF's events. Downstream consumers that filter by tenant must skip these subjects or join through BookingHandoff.tenantId for handoff-attributable events.
3. Events published
3.1 melmastoon.bff.consumer.session.started.v1
When: First request with no gms_ cookie; cookie minted, GuestSession persisted.
Sample rate: 100%
Retention: operational (90 days)
{
"guestSessionId": "gms_01H...",
"createdAt": "2026-04-23T09:00:00.000Z",
"localePreference": "ps-AF",
"currencyPreference": "AFN",
"fingerprintHash": "sha256:9c8b...",
"ipHash": "sha256:1a2b...",
"campaignAttribution": { "source": "google", "medium": "cpc", "campaign": "spring-2026" },
"userAgentClass": "browser-desktop"
}
3.2 melmastoon.bff.consumer.session.ended.v1
When: Explicit POST /session/clear or 30-day TTL elapsed (best-effort sweep).
Sample rate: 100%
Retention: operational
{
"guestSessionId": "gms_01H...",
"endedAt": "2026-04-23T09:14:22.041Z",
"reason": "explicit-clear",
"lastSeenAt": "2026-04-23T09:14:22.041Z",
"lifetimeSeconds": 882
}
reason ∈ explicit-clear | ttl-expired | rotated.
3.3 melmastoon.bff.consumer.search.executed.v1
When: POST /search or POST /search/map returned results (after composition).
Sample rate: 0.1 (default; configurable via Remote Config)
Retention: operational
{
"guestSessionId": "gms_01H...",
"searchSessionId": "srs_01H...",
"queryHash": "sha256:6f3c1...",
"kind": "list",
"geo": { "mode": "city", "city": "Kabul", "country": "AF" },
"dates": { "checkIn": "2026-05-12", "checkOut": "2026-05-15" },
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"filterKeys": ["wifi", "halal-kitchen"],
"sortKey": "recommended",
"page": { "limit": 20, "offset": 0 },
"resultCount": 47,
"fromCache": true,
"compositionMs": 47,
"currency": "AFN",
"locale": "ps-AF"
}
kind ∈ list | map. Note: queryHash allows analytics to dedupe equivalent queries without storing raw filter sets that may include user-typed neighbourhood names.
3.4 melmastoon.bff.consumer.click.recorded.v1
When: POST /telemetry/click accepted.
Sample rate: 100%
Retention: operational
{
"guestSessionId": "gms_01H...",
"kind": "listing-card",
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"searchSessionId": "srs_01H...",
"position": 3,
"page": 1,
"sortKey": "recommended",
"occurredAt": "2026-04-23T09:14:22.041Z"
}
kind ∈ listing-card | map-pin | similar-property | wishlist-card | brand-peek.
3.5 melmastoon.bff.consumer.handoff.initiated.v1
When: Signed handoff token successfully minted.
Sample rate: 100%
Retention: audit (1 year — feeds attribution + fraud investigation)
{
"handoffId": "bhd_01H...",
"guestSessionId": "gms_01H...",
"tenantId": "tnt_01H...",
"tenantSlug": "kabul-grand-hotel",
"propertyId": "ppt_01H...",
"dates": { "checkIn": "2026-05-12", "checkOut": "2026-05-15" },
"occupancy": { "adults": 2, "children": 0, "rooms": 1 },
"currency": "AFN",
"locale": "ps-AF",
"sourceCampaign": { "source": "google", "medium": "cpc", "campaign": "spring-2026" },
"mintedAt": "2026-04-23T09:14:22.041Z",
"expiresAt": "2026-04-23T09:44:22.041Z",
"hmacKeyId": "hmac-2026-04",
"fingerprintHash": "sha256:9c8b...",
"ipHash": "sha256:1a2b...",
"originReferer": "https://www.melmastoon.ghasi.io/search?city=kabul"
}
This is the bridge event that links anonymous-meta-traffic to a downstream booking saga in bff-tenant-booking-service + reservation-service. The receiving BFF emits a paired melmastoon.bff.tenant.bootstrap.served.v1 carrying the same handoffId.
3.6 melmastoon.bff.consumer.locale.changed.v1
{
"guestSessionId": "gms_01H...",
"previousLocale": "en",
"newLocale": "ps-AF",
"trigger": "user-explicit",
"occurredAt": "2026-04-23T09:14:22.041Z"
}
trigger ∈ user-explicit | accept-language | geo-ip.
3.7 melmastoon.bff.consumer.currency.changed.v1
{
"guestSessionId": "gms_01H...",
"previousCurrency": "USD",
"newCurrency": "AFN",
"trigger": "user-explicit",
"occurredAt": "2026-04-23T09:14:22.041Z"
}
trigger ∈ user-explicit | tenant-default | geo-ip.
3.8 melmastoon.bff.consumer.wishlist.added.v1
{
"wishlistId": "wsh_01H...",
"guestSessionId": "gms_01H...",
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"source": "detail",
"addedAt": "2026-04-23T09:14:22.041Z",
"wishlistSize": 5
}
3.9 melmastoon.bff.consumer.wishlist.removed.v1
{
"wishlistId": "wsh_01H...",
"guestSessionId": "gms_01H...",
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"removedAt": "2026-04-23T09:14:22.041Z",
"wishlistSize": 4
}
3.10 melmastoon.bff.consumer.bot_suspected.v1
When: BotDetector returns verdict ∈ {suspect, bot}.
Sample rate: 100%
Retention: audit (1 year — security investigations)
{
"guestSessionId": "gms_01H...",
"score": 0.93,
"verdict": "bot",
"signals": [
{ "kind": "ua-pattern", "weight": 0.4, "detail": "headlessChrome" },
{ "kind": "cadence", "weight": 0.3, "detail": "12 reqs/2s" },
{ "kind": "fingerprint-collision", "weight": 0.23, "detail": "shared-with-94-sessions" }
],
"ipHash": "sha256:1a2b...",
"userAgentHash": "sha256:9c8b...",
"fingerprintHash": "sha256:9c8b...",
"evaluatedAt": "2026-04-23T09:14:22.041Z",
"actionTaken": "challenge"
}
actionTaken ∈ none | challenge | hard-block.
4. Events consumed (cache invalidation only)
4.1 melmastoon.theme.published.v1
Producer: theme-config-service
Effect: Invalidate Memorystore key consumer:brand-peek:<tenantId>.
{
"tenantId": "tnt_01H...",
"themeId": "thm_01H...",
"publishedVersion": "thv_01H...",
"publishedAt": "2026-04-23T09:14:22.041Z"
}
4.2 melmastoon.search_aggregation.listing.indexed.v1
Producer: search-aggregation-service
Effect: Invalidate consumer:detail:<propertyId>:* and best-effort partial-bust of consumer:search:list:* cursor pages overlapping the property's geo cell.
{
"tenantId": "tnt_01H...",
"propertyId": "ppt_01H...",
"indexedAt": "2026-04-23T09:14:22.041Z",
"geoCell": "h3:8a283082aa67fff"
}
4.3 melmastoon.tenant.suspended.v1
Producer: tenant-service
Effect: Add tenantId to in-memory suspendedTenants set (TTL 5 min, refreshed); cause /handoff to that tenant to return MELMASTOON.BFF.CONSUMER.TENANT_SUSPENDED; bust all consumer:* cache keys carrying that tenantId.
{
"tenantId": "tnt_01H...",
"reason": "billing-overdue",
"suspendedAt": "2026-04-23T09:14:22.041Z"
}
4.4 melmastoon.tenant.reinstated.v1
Producer: tenant-service
Effect: Inverse of 4.3 — remove from suspended set, allow /handoff to mint again.
5. Subscriptions
| Subscription name | Topic | Filter | Ack deadline | Max delivery attempts |
|---|---|---|---|---|
bff-consumer.theme.published.v1 | melmastoon.theme.published.v1 | none | 30 s | 5 |
bff-consumer.search_aggregation.listing.indexed.v1 | melmastoon.search_aggregation.listing.indexed.v1 | none | 30 s | 5 |
bff-consumer.tenant.suspended.v1 | melmastoon.tenant.suspended.v1 | none | 30 s | 10 |
bff-consumer.tenant.reinstated.v1 | melmastoon.tenant.reinstated.v1 | none | 30 s | 10 |
DLQ for each subscription: bff-consumer.<topic>.dlq with a paged alert if pull_pending > 100.
6. Schema versioning
- Additive: add new optional fields → no version bump.
- Breaking (renames, removals, type changes, semantic redefinition): publish
*.v2alongside*.v1; deprecate*.v1with adeprecatedSincefield on the schema registry; keep*.v1for 90 days. - Schema validation runs on every PR via the
event-schema-conformanceCI check.
7. Idempotency, ordering, and exactly-once
- The BFF writes outbox rows in the same Postgres transaction as any state mutation (wishlist add, handoff insert, session-blob mirror writes).
- The outbox-relay worker drains rows in
id ASCorder and publishes to Pub/Sub. If a publish fails, the row is retried with exponential backoff; if it stays failed for > 5 min, an alert pages the on-call. - Consumers must dedupe by
envelope.eventId. Per 04 §6.2, every consumer keeps aninboxtable with a 7-day window.
8. Privacy + compliance
- No PII fields are emitted from this BFF.
guestSessionIdis a synthetic identifier;ipHash,fingerprintHash,userAgentHashare SHA-256 with a per-environment pepper. - Retention class
operational→ 90 days in BigQuery analytics dataset;audit→ 1 year in audit-service immutable storage. - DSAR:
guestSessionIdis the join key. A guest can request deletion via the consent banner; this triggersPOST /session/clear+ a downstream sweep job that purges anybff_consumer.*events keyed by thatguestSessionIdfrom BigQuery within 30 days. - No
tenantIdever appears as a tenancy boundary on these events; downstream attribution joins ontenantIdare projection-time only.
9. Sample sequence: campaign click → handoff
client bff-consumer outbox pubsub bff-tenant-booking
│ │ │ │ │
│ GET /search │ │ │ │
├────────────────► │ │ │ │
│ │ search.executed.v1 (sampled) │ │
│ ├──────────────► │ │ │
│ │ │ enqueue │ │
│ │ ├────────────► │ │
│ click │ │ │ │
├────────────────► │ │ │ │
│ │ click.recorded.v1 │ │
│ ├──────────────► │ │ │
│ POST /handoff │ │ │ │
├────────────────► │ │ │ │
│ │ handoff.initiated.v1 (audit) │ │
│ ├──────────────► │ │ │
│ 201 + url │ │ │ │
│ ◄────────────────┤ │ │ │
│ │ │ │ │
│ GET tenant-bff /bootstrap?h=... │
├──────────────────────────────────────────────────────────────────────────►
│ │ │ │ bootstrap.served.v1 (carries handoffId)
│ │ │ │ ◄───────────────────────
10. Reference implementation pointers
- Outbox writer:
src/infrastructure/events/outbox-relay.ts. - Subscription handlers:
src/infrastructure/events/subscriptions/<topic-name>.handler.ts. - Envelope helpers + sampling:
@ghasi/event-envelope. - Schemas:
@ghasi/event-envelope/schemas/bff-consumer/*.schema.json.