Skip to main content

02 — Frontend Architecture Overview

Scope: End-to-end view of the frontend platform: stack per surface, monorepo layout, BFF boundary, sync engine, offline tiers, observability, security baseline. Every other FE doc in this set inherits from here.

Companions: 01-product-overview-frontend.md · 06-theming-and-tenant-config.md · 09-non-functional-requirements.md · ../desktop/21-desktop-app-specification.md

BFF contracts: bff-consumer-service · bff-tenant-booking-service · bff-backoffice-service


1. End-to-end shape

┌──────────────────────────────────────────────────────────────────────┐
│ Browsers / Phones / Desktops │
│ │
│ Consumer Meta Web Tenant Booking Web Guest Portal Web ... │
│ (Next.js 14) (Next.js 14) (Next.js 14) │
│ │
│ Consumer Mobile (Expo / React Native 0.74+) │
│ │
│ Operator Desktop (Electron + Vite + React + SQLite) │
│ Kiosk family (same Electron, kiosk-mode shells) │
└────────────────────────────┬─────────────────────────────────────────┘
│ HTTPS, OpenAPI-typed clients

┌──────────────────────────────────────────────────────────────────────┐
│ Three Backend-for-Frontend services │
│ │
│ bff-consumer-service bff-tenant-booking-service bff-backoffice- │
│ (browse, handoff, (bootstrap, quote, hold, service │
│ wishlist, telemetry) payment-intent, return, (sync, ops, │
│ confirmation) admin) │
└────────────────────────────┬─────────────────────────────────────────┘
│ gRPC / REST internal

┌──────────────────────────────────────────────────────────────────────┐
│ Domain microservices (out of scope for this doc) │
│ property-service · reservation-service · payment-service · │
│ theme-config-service · lock-service · notification-service · ... │
└──────────────────────────────────────────────────────────────────────┘

Three rules govern this picture, always:

  1. App code never imports from another app or calls another app.
  2. App code never calls a domain service directly — only its assigned BFF.
  3. The desktop app additionally runs a sync worker that talks to bff-backoffice-service /sync/v1/pull|push and a local SQLite — see §5.

2. Stack per surface

2.1 Web (Next.js 14, three apps + future)

ConcernChoiceWhy
FrameworkNext.js 14+ App Router; Server Components default; 'use client' for interactive islandsSSR for SEO + first-paint on slow networks; streaming; nested layouts for theme injection
LanguageTypeScript strict (noImplicitAny, strictNullChecks, exactOptionalPropertyTypes, noUncheckedIndexedAccess)Single error surface; lines up with @ghasi/contracts-melmastoon
StylingTailwindCSS via tailwind.preset.ts from @ghasi/ui-melmastoon; CSS variables back every utilityUtility velocity + token enforcement; raw hex lint-blocked
Server stateTanStack Query v5 with persisted query cache (IndexedDB)Aggressive cache reuse on flaky networks
Client stateZustand (with persist middleware to IndexedDB for booking-draft slice)Tiny, no provider tree
URL statenuqsFilters/sort/pagination shareable for SEO
i18nnext-intl with ICU MessageFormatRTL + plurals + complex date formats
Formsreact-hook-form + zod (schemas reused on BFF)One schema per shape
MapsLeaflet (MapTiler default; OSM fallback)Free / low-cost; can self-host
AnimationFramer Motion (sparingly, motion tokens from design system)Page transitions, modal/drawer; reduced-motion respected
PWAnext-pwa or hand-written SW; caches static + tenant bootstrap + last-viewedOffline browse + post-stay
Imagenext/image -> CDN-signed URLs from file-storage-serviceWebP / AVIF / srcset, blur LQIP
HTTPfetch wrapped by typed BFF clients (OpenAPI-generated)Edge-runtime compatible
TestingVitest, Testing Library, Playwright, Lighthouse-CI, ChromaticSee 10-frontend-testing-strategy.md
TelemetryOpenTelemetry web SDK -> Cloud Trace; web-vitals -> BFF telemetry endpointsTrace continuity, RUM
BuildNext.js + Turborepo (incremental, remote-cached)Monorepo speed
Node20 LTSMatch BFF + desktop main process

2.2 Mobile (React Native 0.74+ via Expo)

ConcernChoiceWhy
FrameworkReact Native 0.74+ with New Architecture (Fabric + TurboModules)Modern bridgeless RN; Hermes baseline
Tooling shellExpo (managed -> bare via prebuild when a custom native module forces it)OTA via EAS Update; faster dev cycle
JS engineHermesSmaller bundle, faster startup, better memory on low-end Android
NavigationReact Navigation v7 (native stack + bottom tabs + modal stack)RTL-aware; typed deep linking
Server stateReact Query v5 with react-native-mmkv-backed query persisterSame as web; offline reads
Client stateZustand with MMKV persistBooking draft survives backgrounding
Local storagereact-native-mmkv (sync, fast, encrypted via platform keystore)Cached search/property/booking-draft/session
Animationreact-native-reanimated v3 + react-native-gesture-handler (UI thread)60 fps on low-end Android
Lists@shopify/flash-listSmooth long lists
i18nexpo-localization + @ghasi/i18nLocale parity with web
Pushexpo-notifications (APNs + FCM)Booking confirmations, reminders, key delivery, offers
Biometricexpo-local-authenticationQuick re-auth for Trips tab
Mapsreact-native-maps (Apple iOS, Google Android)Native map perf
Storage encryptioniOS Keychain + Android Keystore for tokens; MMKV encrypted with key from secure storeSecrets not stored in plaintext
TestingJest, React Native Testing Library, Detox, ReassureSee 10-frontend-testing-strategy.md
Observability@sentry/react-native (errors + perf); OTel for trace continuationCrashes + traces
DistributionEAS Build + EAS Submit (TestFlight + Play Console internal track on every main); EAS Update for OTA JS-onlyFast iteration without store review

Single codebase. One RN project produces both iOS and Android. Platform.select is allowed for behavioural differences (Maps provider, biometric prompt) and forbidden for visual differences (those go through tokens).

2.3 Desktop (Electron + Vite + React + SQLite)

Owned by ../desktop/21-desktop-app-specification.md. Headline choices:

  • Electron 30+; Node 20 main; Chromium renderer; Vite + React in renderer
  • better-sqlite3 for the local store; outbox table + sync worker
  • ONNX Runtime Node for local AI when GPU available
  • electron-builder packaging; electron-updater channel per OS
  • USB / serial drivers for lock encoders; receipt printers; cash drawers
  • keytar for OS keychain integration; DPoP-based auth tokens
  • IPC surface exposed via window.melmastoon (typed, ESM)

2.4 Kiosk (Electron in kiosk mode)

Same Electron app, three kiosk-mode shells: self-checkin, housekeeping, arrivals-board. Owned by ../kiosk/22..24.

2.5 Tablet (React Native via Expo, separate apps)

iPadOS / Android tablet apps for front-desk and POS. Owned by ../tablet/25..26. Stack matches mobile RN; UX is touch-first portrait/landscape with secondary screen support.


3. Repository layout (implementation monorepo)

The implementation lives in a separate monorepo named ghasi-melmastoon (this docs repo is ghasi-e-documentation). Layout:

ghasi-melmastoon/
├── pnpm-workspace.yaml
├── turbo.json
├── package.json
├── tsconfig.base.json
├── apps/
│ ├── web-meta/ # Next.js 14 — Consumer Meta Web
│ ├── web-tenant-booking/ # Next.js 14 — Tenant Booking Web
│ ├── web-guest-portal/ # Next.js 14 — Guest Portal (R2)
│ ├── web-control-plane/ # Next.js 14 — Super-admin (R2/R3)
│ ├── mobile/ # React Native (Expo) — Consumer Mobile
│ ├── mobile-staff/ # React Native (Expo) — Staff companion (R2)
│ ├── desktop-backoffice/ # Electron + Vite + React (separate spec)
│ ├── tablet-front-desk/ # React Native (Expo) — Tablet (R3)
│ └── tablet-pos/ # React Native (Expo) — Tablet POS (R3)
├── packages/
│ ├── ui-melmastoon/ # @ghasi/ui-melmastoon — design system
│ ├── i18n/ # @ghasi/i18n — message bundles + ICU runtime
│ ├── api-clients/ # @ghasi/api-clients — typed BFF clients (OpenAPI-generated)
│ ├── icons/ # @ghasi/icons — line/filled SVGs + RN equivalents
│ ├── feature-meta-search/ # cross-app: search, filters, map<->list sync
│ ├── feature-booking-flow/ # cross-app: booking funnel state machine
│ ├── feature-housekeeping/ # cross-app: HK board (desktop + tablet + mobile-staff)
│ ├── feature-ai-surfaces/ # cross-app: copilot/concierge UI primitives
│ ├── contracts-melmastoon/ # branded types, ULIDs, AIProvenance, MediaRef, Locale
│ ├── domain-primitives/ # tenant-agnostic primitives
│ ├── eslint-config/ # shared ESLint rules (incl. token-only)
│ └── tsconfig/ # shared tsconfigs
├── tooling/
│ ├── changesets/
│ └── ci/ # Lighthouse-CI configs, Reassure thresholds
└── .github/workflows/

Tooling: pnpm workspaces, Turborepo (Vercel Remote Cache or self-hosted), Changesets for semver of internal packages.

Module boundaries. Apps depend on packages; packages depend only on packages with a strictly lower position in a layered DAG (ui-melmastoon -> icons + i18n; feature-* -> ui-melmastoon + api-clients). Apps never import from each other. Enforced by eslint-plugin-boundaries.


4. State model (cross-surface)

LayerOwnerPersistenceExamples
Server stateReact QueryIn-memory + IndexedDB (web) / MMKV (mobile) / SQLite (desktop)searchHotels, getHotel, getBootstrap, getQuote
Client stateZustandIndexedDB (web) / MMKV (mobile) / SQLite (desktop) for booking-draft + ephemeral UIBooking draft, filter-panel-open, map-vs-list toggle
URL statenuqs (web) / React Navigation params (mobile)URL itselfFilters, sort key, pagination, map bounds
Session stateBFF + cookie / DPoP tokenServer-side (Memorystore) keyed by gms_id (consumer) or tnt_id (tenant)Locale, currency, wishlist (dual-storage: cookie pre-auth, account post-auth, merge on login), recently-viewed

4.1 React Query key conventions (verbatim)

queryKey: ['consumer.v1', 'search', sanitizedQuery]
queryKey: ['consumer.v1', 'hotel', propertyId]
queryKey: ['consumer.v1', 'hotel', propertyId, 'availability', { from, to, adults, rooms }]
queryKey: ['consumer.v1', 'wishlist']

queryKey: ['tenant.v1', tenantSlug, 'bootstrap']
queryKey: ['tenant.v1', tenantSlug, 'availability', { propertyId, checkIn, checkOut, ... }]
queryKey: ['tenant.v1', tenantSlug, 'draft', draftId]

queryKey: ['backoffice.v1', tenantSlug, 'reservations', { date, status }]
queryKey: ['backoffice.v1', tenantSlug, 'housekeeping', { wing, date }]

staleTime and gcTime mirror the BFF's Cache-Control directives. Mutations invalidate the minimal set.

4.2 Booking-draft state machine (Zustand, summarised)

type BookingDraftState =
| { kind: 'idle' }
| { kind: 'searching'; criteria: SearchCriteria }
| { kind: 'selectingRoom'; tenantSlug: string; propertyId: string; criteria: SearchCriteria }
| { kind: 'quoting'; quoteId: string; ... }
| { kind: 'holding'; draftId: string; reservationId: string; holdExpiresAt: ISODate; ... }
| { kind: 'collectingDetails'; draftId: string; guest: GuestForm; ... }
| { kind: 'paying'; draftId: string; intentId: string; redirectUrl?: string; ... }
| { kind: 'awaitingReturn'; draftId: string; intentId: string; ... }
| { kind: 'confirmed'; reservationId: string }
| { kind: 'failed'; reason: BookingFailureReason; recoverable: boolean };

Transitions are explicit functions (startSearch, selectRoom, quote, hold, patchGuest, createIntent, handleReturn). kind: 'holding' | 'collectingDetails' | 'paying' are persisted; kind: 'idle' | 'searching' are not.


5. Sync and offline tiers

Three explicit tiers — the same tenant data is treated very differently on each runtime:

TierSurfacesLocal storeSync engineRecovery on lost connectivity
Tier 1 — browse cacheConsumer Meta Web; Tenant Booking Web (read pages); Guest Portal Web (post-stay)Service Worker + IndexedDB (last bootstrap, last 20 properties, last 50 search results)none (read-only)Banner; re-fetch on reconnect; no writes possible
Tier 2 — read cache + mutation queueConsumer Mobile; Staff Mobile CompanionMMKV (last 50 search, last 20 viewed, booking draft)none (read cache only) — writes require connectivity"You're offline" banner; CTAs requiring writes are disabled, not silent
Tier 3 — offline-firstOperator Desktop; Kiosk family (front desk, HK, arrivals)better-sqlite3 (full operating data set for the property) + outbox tablebidirectional pull/push against bff-backoffice-service /sync/v1/* with Lamport clocks; conflict policy per-aggregate (per ../../06-data-models.md)Continues operating; outbox replays on reconnect; conflict UI surfaces unresolved cases for human decision

Conflict policy per aggregate:

  • Reservation — server-wins on financials; client-wins on housekeeping notes
  • RoomStatus — last-writer-wins on cleaning|dirty|inspected|out_of_order (idempotent transitions)
  • GuestProfile — server-wins on identity fields; client-wins on stay preferences
  • RatePlan — server-wins (always); client mutations rejected with explicit "rate plans are central"

Full conflict matrix in ../desktop/21-desktop-app-specification.md §11.


6. Security and tenancy boundary

  • JWT + DPoP for desktop / kiosk; cookies for web; secure store for mobile
  • X-Tenant-Id header on every authenticated request; rejected if mismatched
  • CSP per-route with nonces; connect-src allow-lists per BFF host
  • Cross-tenant theme leak prevention: tenant theme injected only after tenant resolution at BFF; suspended-tenant pages are platform-rendered (no theme leak)
  • PII handling: PII never on telemetry surface; only minimal IDs in cookies; mobile MMKV encrypted

Full security model: ../../07-security-compliance-tenancy.md. Per-BFF security details live in each SECURITY_MODEL.md.


7. Observability baseline

  • OpenTelemetry web SDK + RN OTel + Electron OTel; spans propagate traceparent to BFF
  • Web Vitals via web-vitals -> BFF telemetry endpoint -> Cloud Monitoring dashboard
  • RN crashes + perf via @sentry/react-native; sourcemap-resolved in Sentry
  • Desktop telemetry via OTel from main + renderer + sync worker; resource attributes include app.version, os.name, tenant.id, property.id
  • No vendor SDK proliferation — telemetry goes through @ghasi/telemetry which adapters to OTel + Sentry only

Event naming convention follows ../catalogs/C1-telemetry-event-dictionary.md (when present in P1).


8. Platform parity matrix

Capability availability per surface — Y = supported now, Y2 = R2, Y3 = R3, - = not applicable.

CapabilityMeta WebTenant Booking WebGuest PortalConsumer MobileStaff MobileOperator DesktopKioskTablet FDTablet POS
Search & discoveryY--Y-Y (admin)---
Booking funnel (guest)-Y-Y-Y (front desk)Y (self-checkin)Y-
Cash-on-arrival-Y-Y-YYY-
Card payment-YYY-YYYY
Mobile money (MFS)-YYY-YYY-
Multi-tenant theming-YYYYYYYY
RTL (PS/DR/UR/AR)YYYYYYYYY
Offline readY (PWA)Y (PWA)Y2YYY (full)Y (full)Y2Y2
Offline write----Y2YYY3Y3
Local AI (ONNX)-----YY--
Push notificationsY2 (web push)Y2Y2YY2----
Biometric login--Y2YY2Y2 (TouchID/WindowsHello)-Y2Y2
BLE / NFC (lock)---Y2-Y (encoder via USB)Y--
Apple/Google Wallet--Y2Y2-----
Camera (passport / MRZ)--Y2Y2Y2Y (USB scanner)YY-
Receipt printer-----YYYY
Cash drawer-----YYYY
Operator copilot (AI)----Y2Y2-Y2Y2
Guest concierge (AI)Y2Y2Y2Y2-----
Owner Insight (AI)-----Y3---
AR room preview-Y3-Y3-----
Page builder (admin)-----Y2---

This matrix is the canonical answer to "where does X work?". Update it any time a capability lands or moves phase.


9. Anti-patterns (verbatim ban list)

Anti-patternWhy it's bannedCorrect approach
Calling domain microservices directly from web/mobile/desktopBypasses tenant resolution, theming, rate limiting, CSP, auditAlways go through the appropriate BFF
if (tenantId === 'tnt_acme') { ... } in shared codeTenant-coupled code prevents safe deploysDrive variation through theme-config-service
Importing Tailwind raw colors (bg-blue-500)Defeats tenant theming; breaks contrast invariantsUse semantic tokens (bg-primary, text-on-surface)
Hardcoded English strings or aria-labelsBreaks i18n; trips RTL auditsAlways go through next-intl / @ghasi/i18n
Heavy synchronous work on the main threadBlocks INP; tanks low-end AndroidOff-thread via Worker (web) / runOnJS / native module (mobile)
useEffect for deriving render stateHydration flicker, re-render stormsCompute during render or useMemo / useSyncExternalStore
Storing booking PII in localStorage / AsyncStorageXSS-readable; breaks session-clear UXServer-side session blob; minimal IDs in cookies; mobile MMKV encrypted
Direct <a href="https://other-tenant.com"> cross-tenant linksLeaks tenant branding into another tenant contextAlways route via POST /handoff/{tenant}/{property}
Per-tenant React component filesDoesn't scale; contradicts single-codebase commitmentCompose primitives + content blocks driven by config
dangerouslySetInnerHTML from BFF without explicit sanitizationXSS surfaceRender via primitives that consume the sanitized I18nMarkup from the BFF
OTA-pushing native codeBreaks store policiesNative changes go through EAS Build + store review only
Skipping X-Idempotency-Key on POSTsDouble-bookings, double-charges, double-handoffsAlways generate a ULID per attempt and reuse on retry
Bypassing the sync worker on desktopCauses silent data divergenceAll writes go through outbox; no direct SQLite mutations from renderer

References