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).
- Selected tenant is
- Trigger: Guest taps "Book on hotel site" on the property detail screen (J-01 step 4.5).
3. Entry Points
| # | Entry | Notes |
|---|---|---|
| 1 | "Book on hotel site" CTA on PropertyDetailScreen (J-01) | Default flow |
| 2 | Direct landing on <tenantSlug>.melmastoon.app | No handoff token; /bootstrap runs without pre-fill |
| 3 | Universal Link / App Link from social share | Handoff token in URL; mobile opens app if installed, web fallback otherwise |
| 4 | OTA 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 / Mutation | Key | Endpoint | Notes |
|---|---|---|---|
mintHandoff (mutation) | n/a | POST /api/v1/handoff on bff-consumer-service | Body: { tenantId, propertyId, search, locale, currency, traceId }; returns { url, expiresAt }; HMAC-signed; 5 min TTL |
consumeHandoff (server action / SSR) | n/a | POST /api/v1/handoff/consume on bff-tenant-booking-service | Single-use; sets consumed=true |
getBootstrap | ['tenant.v1', tenantSlug, 'bootstrap'] | GET /api/v1/bootstrap on bff-tenant-booking-service | Returns 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_tenantsmini-list in IndexedDB / MMKV (last 5).
6.4 Idempotency
POST /handoffcarriesX-Idempotency-Key(ULID per attempt); replays return same handoff URL.POST /handoff/consumeis single-use; idempotent retry on the same token returns the sameconsumed=trueoutcome.
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
| Error | Trigger | UX shown | Recovery | Telemetry |
|---|---|---|---|---|
HANDOFF_LINK_EXPIRED | TTL > 5 min | Page: "This booking session expired - start again" + back link to meta property | Click back -> mints fresh handoff transparently | frontend.tenant.handoff_invalid { reason: "expired" } |
HANDOFF_TENANT_UNKNOWN | Slug suspended / deleted / renamed | Page: "This hotel isn't accepting direct bookings right now" + CTA back to meta | Manual back to meta | frontend.tenant.handoff_invalid { reason: "unknown_tenant" } |
HANDOFF_TENANT_SUSPENDED | Tenant in suspended state | Platform-rendered "temporarily unavailable" page; no tenant theme leak | Manual back; subscribe to "Notify when available" (Phase 2) | frontend.tenant.handoff_invalid { reason: "suspended" } |
THEME_VERSION_UNPUBLISHED | Mid-deploy revert | Falls back to last known good version + internal alert to GM via desktop feed | Auto-recovery on next theme publish | frontend.tenant.theme_fallback { tenantId } |
MINT_5XX | bff-consumer-service failure | Inline error on PropertyDetail with retry CTA; user remains on meta | Manual retry; auto-retry once on transient | error.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
| Metric | Target |
|---|---|
| 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
| Event | When | Properties |
|---|---|---|
frontend.meta.handoff_started | CTA click | tenantId, propertyId, traceId |
frontend.tenant.booking_session_started | Tenant bootstrap success | tenantId, source (meta-handoff / direct / app-link / partner) |
frontend.tenant.theme_loaded | Tenant theme tokens applied | themeVersion, latency_ms |
frontend.tenant.handoff_invalid | Any handoff error | reason |
Domain events emitted
melmastoon.bff_consumer.handoff.issued.v1melmastoon.bff_consumer.handoff.consumed.v1melmastoon.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.