Skip to main content

J-02 — Booking Handoff to Tenant Site

One-liner: Transition the guest from the meta layer into the tenant-themed booking experience without losing dates / guests / locale.

1. Purpose

Carry the guest's intent (selected property, dates, guests, rooms, locale, currency) from the meta layer into the tenant-themed booking experience without losing context, and to do it fast enough that the guest does not perceive a domain switch as friction. Outcome: tenant booking site loads in <= 1.5 s p95 on a 3G profile with the guest's criteria pre-populated.

2. Persona Context

  • Persona: Guest.
  • Surfaces: Consumer Meta Web / Consumer Mobile (initiator) -> Tenant Booking Web (<tenantSlug>.melmastoon.app) (target).
  • Primary BFFs: bff-consumer-service (mints handoff) -> bff-tenant-booking-service (consumes handoff + bootstrap).
  • Backing services: theme-config-service (published theme), tenant-service (slug -> tenantId).
  • Preconditions:
    • Selected tenant is active (not suspended) and has a published theme version.
    • At least one published rate plan exists for the property.
    • Both BFFs share the platform's request-context middleware (tenant resolution, locale, traceparent propagation).
  • Trigger: Guest taps "Book on hotel site" on the property detail screen (J-01 step 4.5).

3. Entry Points

#EntryNotes
1"Book on hotel site" CTA on PropertyDetailScreen (J-01)Default flow
2Direct landing on <tenantSlug>.melmastoon.appNo handoff token; /bootstrap runs without pre-fill
3Universal Link / App Link from social shareHandoff token in URL; mobile opens app if installed, web fallback otherwise
4OTA partner deep link (Phase 2)Same handoff mechanism with partner attribution

4. Screen-by-Screen Flow

4.1 HandoffTransition (loading bridge, ephemeral)

  • Layout: Full-screen dimmed overlay with branded loading indicator and copy "Connecting you to ..." in user's locale; max display time 1.5 s p95.
  • Components: LoadingOverlay, BrandTransitionMark.
  • Offline: CTA on PropertyDetailScreen is disabled when offline; this screen is never reached offline.
  • AI: None.
  • Errors: If POST /handoff/{tenant}/{property} fails, fall back to inline error on PropertyDetail with retry; transition does not display.
  • Loading: Skeleton optional; spinner is acceptable here because perceived duration < 1.5 s.
  • A11y: aria-live="polite" announces "Connecting to "; focus is parked on a <h1 tabIndex={-1}> containing the tenantName.
  • RTL: Loading copy + spinner mirror.
  • Perf: TTI <= 200 ms (no app bundle change yet); end-to-end handoff <= 1.5 s p95.
  • Telemetry: frontend.meta.handoff_started { tenantId, propertyId, traceId }.

4.2 BootstrapTenantScreen (Tenant Booking Web first paint)

  • Layout: Tenant-branded shell (logo, tokens applied SSR), hero photo, room-type cards, sticky CTA "Continue to booking"; dates and guests pre-populated in the inline summary.
  • Components: TenantShellHeader, Hero, RoomTypeCard, StickyCta.
  • Offline: Tenant Booking Web has a per-tenant PWA cache; if cached, the shell renders with cached bootstrap and a "Latest data unavailable - try again" banner; CTA disabled.
  • AI: None at bootstrap; subsequent steps may include name transliteration etc.
  • Errors: Signed-link expired -> "This booking session expired - start again"; unknown tenant slug -> "This hotel isn't accepting direct bookings right now"; theme unpublished -> falls back to last known good with internal alert.
  • Loading: SSR delivers shell + tokens in HTML; client hydration <= 200 ms.
  • A11y: Page heading focus on route change; tenant name announced; locale switcher in header.
  • RTL: Full RTL flip if tenant default locale is RTL; per-tenant override possible.
  • Perf: SSR ensures no theme flicker; LCP element = hero photo with WebP/AVIF; <= 2 s on 3G.
  • Telemetry: view.page { path=/, tenantId }; frontend.tenant.booking_session_started { tenantId, source }; frontend.tenant.theme_loaded { themeVersion, latency_ms }.

5. State Machine

6. Data Requirements

6.1 Server state

Query / MutationKeyEndpointNotes
mintHandoff (mutation)n/aPOST /api/v1/handoff on bff-consumer-serviceBody: { tenantId, propertyId, search, locale, currency, traceId }; returns { url, expiresAt }; HMAC-signed; 5 min TTL
consumeHandoff (server action / SSR)n/aPOST /api/v1/handoff/consume on bff-tenant-booking-serviceSingle-use; sets consumed=true
getBootstrap['tenant.v1', tenantSlug, 'bootstrap']GET /api/v1/bootstrap on bff-tenant-booking-serviceReturns theme tokens + flow config + content blocks + locale packs

6.2 URL state

  • Source: <consumerMeta> -> redirect target: https://<tenantSlug>.melmastoon.app/book?h=<token>
  • ?h= carries the signed token; consumed once on first SSR render; subsequent navigation does not require it.

6.3 Local persistence

  • Tenant bootstrap cached for 24 h in PWA SW (per tenant).
  • recently_used_tenants mini-list in IndexedDB / MMKV (last 5).

6.4 Idempotency

  • POST /handoff carries X-Idempotency-Key (ULID per attempt); replays return same handoff URL.
  • POST /handoff/consume is single-use; idempotent retry on the same token returns the same consumed=true outcome.

7. AI Behavior

n/a — pure routing + tenant resolution.

8. Offline Behavior

  • The "Book on hotel site" CTA is disabled when the consumer surface detects no connectivity.
  • Tenant subdomain entry while offline: SW serves cached shell + last bootstrap (if cached) with a banner + CTA disabled until reconnect.
  • Mobile deep link while offline opens cached app shell; user is shown the offline banner; tap "Try again" re-attempts on reconnect.

9. Error States

ErrorTriggerUX shownRecoveryTelemetry
HANDOFF_LINK_EXPIREDTTL > 5 minPage: "This booking session expired - start again" + back link to meta propertyClick back -> mints fresh handoff transparentlyfrontend.tenant.handoff_invalid { reason: "expired" }
HANDOFF_TENANT_UNKNOWNSlug suspended / deleted / renamedPage: "This hotel isn't accepting direct bookings right now" + CTA back to metaManual back to metafrontend.tenant.handoff_invalid { reason: "unknown_tenant" }
HANDOFF_TENANT_SUSPENDEDTenant in suspended statePlatform-rendered "temporarily unavailable" page; no tenant theme leakManual back; subscribe to "Notify when available" (Phase 2)frontend.tenant.handoff_invalid { reason: "suspended" }
THEME_VERSION_UNPUBLISHEDMid-deploy revertFalls back to last known good version + internal alert to GM via desktop feedAuto-recovery on next theme publishfrontend.tenant.theme_fallback { tenantId }
MINT_5XXbff-consumer-service failureInline error on PropertyDetail with retry CTA; user remains on metaManual retry; auto-retry once on transienterror.surfaced { code, traceId }

10. E2E Test Gates

  • Part of composite gate G-WEB-1 (search -> handoff -> bootstrap -> ...).
  • Direct gate test: handoff token expired flow + transparent re-mint.
  • Tenant suspended fallback rendered correctly (no theme leak).

11. Performance Requirements

MetricTarget
Handoff mint (POST /handoff RTT)< 200 ms p95
Tenant subdomain TTFB< 600 ms p75 (CDN-warmed)
End-to-end handoff (CTA click -> tenant booking ready)< 1.5 s p95 on 3G
Theme injection (no flicker)0 hydration mismatches in production telemetry

12. Accessibility Requirements

  • aria-live="polite" announces transition copy (Connecting to <tenantName>).
  • Focus moves to <h1> of the tenant booking page on landing.
  • Skip-to-content link present in tenant shell.
  • Loading copy is keyboard-dismissible (Esc returns to meta).

13. Telemetry

Frontend events

EventWhenProperties
frontend.meta.handoff_startedCTA clicktenantId, propertyId, traceId
frontend.tenant.booking_session_startedTenant bootstrap successtenantId, source (meta-handoff / direct / app-link / partner)
frontend.tenant.theme_loadedTenant theme tokens appliedthemeVersion, latency_ms
frontend.tenant.handoff_invalidAny handoff errorreason

Domain events emitted

  • melmastoon.bff_consumer.handoff.issued.v1
  • melmastoon.bff_consumer.handoff.consumed.v1
  • melmastoon.bff_tenant_booking.session.started.v1

14. Success Criteria

  • Handoff completes in <= 1.5 s p95 on a 3G profile.
  • Pre-populated dates / guests are identical to the meta-layer search criteria.
  • The signed token is single-use; replay attempts produce a deterministic "expired" UX.
  • Suspended-tenant handoff renders the platform-themed page with zero tenant theme leak.
  • Theme version unpublished gracefully falls back; an internal alert reaches the GM within 60 s.
  • Hydration flicker count = 0 in production.

References