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.mdBFF 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:
- App code never imports from another app or calls another app.
- App code never calls a domain service directly — only its assigned BFF.
- The desktop app additionally runs a sync worker that talks to
bff-backoffice-service /sync/v1/pull|pushand a local SQLite — see §5.
2. Stack per surface
2.1 Web (Next.js 14, three apps + future)
| Concern | Choice | Why |
|---|---|---|
| Framework | Next.js 14+ App Router; Server Components default; 'use client' for interactive islands | SSR for SEO + first-paint on slow networks; streaming; nested layouts for theme injection |
| Language | TypeScript strict (noImplicitAny, strictNullChecks, exactOptionalPropertyTypes, noUncheckedIndexedAccess) | Single error surface; lines up with @ghasi/contracts-melmastoon |
| Styling | TailwindCSS via tailwind.preset.ts from @ghasi/ui-melmastoon; CSS variables back every utility | Utility velocity + token enforcement; raw hex lint-blocked |
| Server state | TanStack Query v5 with persisted query cache (IndexedDB) | Aggressive cache reuse on flaky networks |
| Client state | Zustand (with persist middleware to IndexedDB for booking-draft slice) | Tiny, no provider tree |
| URL state | nuqs | Filters/sort/pagination shareable for SEO |
| i18n | next-intl with ICU MessageFormat | RTL + plurals + complex date formats |
| Forms | react-hook-form + zod (schemas reused on BFF) | One schema per shape |
| Maps | Leaflet (MapTiler default; OSM fallback) | Free / low-cost; can self-host |
| Animation | Framer Motion (sparingly, motion tokens from design system) | Page transitions, modal/drawer; reduced-motion respected |
| PWA | next-pwa or hand-written SW; caches static + tenant bootstrap + last-viewed | Offline browse + post-stay |
| Image | next/image -> CDN-signed URLs from file-storage-service | WebP / AVIF / srcset, blur LQIP |
| HTTP | fetch wrapped by typed BFF clients (OpenAPI-generated) | Edge-runtime compatible |
| Testing | Vitest, Testing Library, Playwright, Lighthouse-CI, Chromatic | See 10-frontend-testing-strategy.md |
| Telemetry | OpenTelemetry web SDK -> Cloud Trace; web-vitals -> BFF telemetry endpoints | Trace continuity, RUM |
| Build | Next.js + Turborepo (incremental, remote-cached) | Monorepo speed |
| Node | 20 LTS | Match BFF + desktop main process |
2.2 Mobile (React Native 0.74+ via Expo)
| Concern | Choice | Why |
|---|---|---|
| Framework | React Native 0.74+ with New Architecture (Fabric + TurboModules) | Modern bridgeless RN; Hermes baseline |
| Tooling shell | Expo (managed -> bare via prebuild when a custom native module forces it) | OTA via EAS Update; faster dev cycle |
| JS engine | Hermes | Smaller bundle, faster startup, better memory on low-end Android |
| Navigation | React Navigation v7 (native stack + bottom tabs + modal stack) | RTL-aware; typed deep linking |
| Server state | React Query v5 with react-native-mmkv-backed query persister | Same as web; offline reads |
| Client state | Zustand with MMKV persist | Booking draft survives backgrounding |
| Local storage | react-native-mmkv (sync, fast, encrypted via platform keystore) | Cached search/property/booking-draft/session |
| Animation | react-native-reanimated v3 + react-native-gesture-handler (UI thread) | 60 fps on low-end Android |
| Lists | @shopify/flash-list | Smooth long lists |
| i18n | expo-localization + @ghasi/i18n | Locale parity with web |
| Push | expo-notifications (APNs + FCM) | Booking confirmations, reminders, key delivery, offers |
| Biometric | expo-local-authentication | Quick re-auth for Trips tab |
| Maps | react-native-maps (Apple iOS, Google Android) | Native map perf |
| Storage encryption | iOS Keychain + Android Keystore for tokens; MMKV encrypted with key from secure store | Secrets not stored in plaintext |
| Testing | Jest, React Native Testing Library, Detox, Reassure | See 10-frontend-testing-strategy.md |
| Observability | @sentry/react-native (errors + perf); OTel for trace continuation | Crashes + traces |
| Distribution | EAS Build + EAS Submit (TestFlight + Play Console internal track on every main); EAS Update for OTA JS-only | Fast iteration without store review |
Single codebase. One RN project produces both iOS and Android.
Platform.selectis 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-sqlite3for the local store;outboxtable + sync worker- ONNX Runtime Node for local AI when GPU available
electron-builderpackaging;electron-updaterchannel per OS- USB / serial drivers for lock encoders; receipt printers; cash drawers
keytarfor 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)
| Layer | Owner | Persistence | Examples |
|---|---|---|---|
| Server state | React Query | In-memory + IndexedDB (web) / MMKV (mobile) / SQLite (desktop) | searchHotels, getHotel, getBootstrap, getQuote |
| Client state | Zustand | IndexedDB (web) / MMKV (mobile) / SQLite (desktop) for booking-draft + ephemeral UI | Booking draft, filter-panel-open, map-vs-list toggle |
| URL state | nuqs (web) / React Navigation params (mobile) | URL itself | Filters, sort key, pagination, map bounds |
| Session state | BFF + cookie / DPoP token | Server-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:
| Tier | Surfaces | Local store | Sync engine | Recovery on lost connectivity |
|---|---|---|---|---|
| Tier 1 — browse cache | Consumer 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 queue | Consumer Mobile; Staff Mobile Companion | MMKV (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-first | Operator Desktop; Kiosk family (front desk, HK, arrivals) | better-sqlite3 (full operating data set for the property) + outbox table | bidirectional 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 notesRoomStatus— last-writer-wins oncleaning|dirty|inspected|out_of_order(idempotent transitions)GuestProfile— server-wins on identity fields; client-wins on stay preferencesRatePlan— 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-Idheader on every authenticated request; rejected if mismatched- CSP per-route with nonces;
connect-srcallow-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
traceparentto 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/telemetrywhich 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.
| Capability | Meta Web | Tenant Booking Web | Guest Portal | Consumer Mobile | Staff Mobile | Operator Desktop | Kiosk | Tablet FD | Tablet POS |
|---|---|---|---|---|---|---|---|---|---|
| Search & discovery | Y | - | - | Y | - | Y (admin) | - | - | - |
| Booking funnel (guest) | - | Y | - | Y | - | Y (front desk) | Y (self-checkin) | Y | - |
| Cash-on-arrival | - | Y | - | Y | - | Y | Y | Y | - |
| Card payment | - | Y | Y | Y | - | Y | Y | Y | Y |
| Mobile money (MFS) | - | Y | Y | Y | - | Y | Y | Y | - |
| Multi-tenant theming | - | Y | Y | Y | Y | Y | Y | Y | Y |
| RTL (PS/DR/UR/AR) | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Offline read | Y (PWA) | Y (PWA) | Y2 | Y | Y | Y (full) | Y (full) | Y2 | Y2 |
| Offline write | - | - | - | - | Y2 | Y | Y | Y3 | Y3 |
| Local AI (ONNX) | - | - | - | - | - | Y | Y | - | - |
| Push notifications | Y2 (web push) | Y2 | Y2 | Y | Y2 | - | - | - | - |
| Biometric login | - | - | Y2 | Y | Y2 | Y2 (TouchID/WindowsHello) | - | Y2 | Y2 |
| BLE / NFC (lock) | - | - | - | Y2 | - | Y (encoder via USB) | Y | - | - |
| Apple/Google Wallet | - | - | Y2 | Y2 | - | - | - | - | - |
| Camera (passport / MRZ) | - | - | Y2 | Y2 | Y2 | Y (USB scanner) | Y | Y | - |
| Receipt printer | - | - | - | - | - | Y | Y | Y | Y |
| Cash drawer | - | - | - | - | - | Y | Y | Y | Y |
| Operator copilot (AI) | - | - | - | - | Y2 | Y2 | - | Y2 | Y2 |
| Guest concierge (AI) | Y2 | Y2 | Y2 | Y2 | - | - | - | - | - |
| 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-pattern | Why it's banned | Correct approach |
|---|---|---|
| Calling domain microservices directly from web/mobile/desktop | Bypasses tenant resolution, theming, rate limiting, CSP, audit | Always go through the appropriate BFF |
if (tenantId === 'tnt_acme') { ... } in shared code | Tenant-coupled code prevents safe deploys | Drive variation through theme-config-service |
Importing Tailwind raw colors (bg-blue-500) | Defeats tenant theming; breaks contrast invariants | Use semantic tokens (bg-primary, text-on-surface) |
| Hardcoded English strings or aria-labels | Breaks i18n; trips RTL audits | Always go through next-intl / @ghasi/i18n |
| Heavy synchronous work on the main thread | Blocks INP; tanks low-end Android | Off-thread via Worker (web) / runOnJS / native module (mobile) |
useEffect for deriving render state | Hydration flicker, re-render storms | Compute during render or useMemo / useSyncExternalStore |
| Storing booking PII in localStorage / AsyncStorage | XSS-readable; breaks session-clear UX | Server-side session blob; minimal IDs in cookies; mobile MMKV encrypted |
Direct <a href="https://other-tenant.com"> cross-tenant links | Leaks tenant branding into another tenant context | Always route via POST /handoff/{tenant}/{property} |
| Per-tenant React component files | Doesn't scale; contradicts single-codebase commitment | Compose primitives + content blocks driven by config |
dangerouslySetInnerHTML from BFF without explicit sanitization | XSS surface | Render via primitives that consume the sanitized I18nMarkup from the BFF |
| OTA-pushing native code | Breaks store policies | Native changes go through EAS Build + store review only |
Skipping X-Idempotency-Key on POSTs | Double-bookings, double-charges, double-handoffs | Always generate a ULID per attempt and reuse on retry |
| Bypassing the sync worker on desktop | Causes silent data divergence | All writes go through outbox; no direct SQLite mutations from renderer |
References
README— frontend index and source-of-truth hierarchy01-product-overview-frontend.md06-theming-and-tenant-config.md09-non-functional-requirements.md../desktop/21-desktop-app-specification.md../../02-enterprise-architecture.md../../05-api-design.md../../06-data-models.md../../07-security-compliance-tenancy.md