Skip to main content

bff-backoffice-service

Backend-for-Frontend for the Electron desktop backoffice application used by hotel staff (front desk, housekeeping, maintenance, F&B managers, GMs). This BFF is a stateless orchestration layer over our domain services. It owns no domain state, makes no domain mutations directly, and emits no domain events. When the desktop is offline, it bypasses this BFF entirely and reads/writes its local SQLite store; this BFF is only on the path when the desktop is online.

FieldValue
OwnerFrontend Platform team
ScopeSingle bounded context: backoffice desktop session orchestration
StagePhase 1 (greenfield)
Service IDbff-backoffice-service
ClassBFF (no domain state, stateless on hot path)
Naming rootMELMASTOON.BFF.BACKOFFICE.*
Event subjectsmelmastoon.bff.backoffice.*
Database scopePostgres for outbox + idempotency + sync-cursor cache + activity ledger; Memorystore for sessions + dashboard cache

1. Purpose

The desktop client is offline-first (ADR-0003) — when the network is up, it talks to this BFF for: dashboard composition, workbench reads, sync handshake (proxy to sync-service), AI suggestion fetch (proxy to ai-orchestrator-service), lock-action proxying (to lock-integration-service), reservation/folio mutations (to domain services with idempotency keys), device session lifecycle, alert acknowledgment, and operator preference sync. When the network is down, the desktop talks to its local SQLite and queues outbox writes; on reconnect it drains via this BFF (more precisely, via this BFF for non-sync-protocol calls, and directly to sync-service for the bulk pull/push protocol — this BFF orchestrates the cursor handshake but does not proxy bulk binary streams).

2. Why a BFF (not direct-to-service)

  • The desktop renders ~30 widgets per dashboard pull. A direct fanout from the renderer to 8 services over potentially flaky links is wasteful. The BFF composes one chunked SSR stream.
  • Device-bound JWT lifecycle (iam-service.refresh with device proof-of-possession) is centralized here, not duplicated across the desktop and three other client surfaces.
  • AI suggestion polling/SSE multiplexing keeps the desktop from holding a long connection per suggestion class.
  • Telemetry shape (operator activity, dashboard view, sync-cursor advance, device heartbeat) is BFF-owned, normalized across staff workstations.
  • Lock-action proxying carries audit context (operator_id, device_id, reservation_id, mfa_attestation) that the desktop should not need to assemble.

3. Aggregates owned (session/projection only)

AggregateStorageNote
BackofficeSessionMemorystore (TTL 12 h)Operator + device + tenant + property scope
DashboardSnapshotMemorystore (TTL 30 s)Composed dashboard payload, per (tenantId, propertyId, operatorRole)
WorkbenchViewMemorystore (TTL 15 s)Per-feature view: front-desk grid, housekeeping board, maintenance board
AISuggestionInboxMemorystore + Postgres lightweight logMirror of orchestrator inbox; decisions logged for audit
AlertInboxMemorystore + Postgres ack logOperator-acknowledged alerts
OperatorActivityPostgres ledger (90 d)Operator action telemetry
DeviceSyncStatusPostgres + MemorystorePer-device cursor cache + heartbeat
OperatorPreferencesMemorystore (read-through) + Postgres mirrorLayout, density, columns, shortcuts
KeyboardShortcutMapStatic + Postgres overridesServer-rendered map for the desktop renderer
OfflineActionQueueHintPostgres (informational only)Hint of what desktop queued; desktop owns the canonical queue locally

The BFF holds no reservation, folio, lock, room, rate, inventory, or guest data. Those live in their respective services.

4. Hot endpoints

MethodPathPurpose
GET/bff/backoffice/v1/dashboard?propertyIdComposed dashboard SSR payload
GET/bff/backoffice/v1/today?propertyIdQuick "today at a glance" panel
GET/bff/backoffice/v1/arrivals?propertyId&dateToday's expected arrivals
GET/bff/backoffice/v1/departures?propertyId&dateToday's expected departures
GET/bff/backoffice/v1/in-house?propertyIdIn-house guests
GET/bff/backoffice/v1/housekeeping/board?propertyIdHousekeeping board view
GET/bff/backoffice/v1/maintenance/board?propertyIdMaintenance board view
GET/bff/backoffice/v1/ai/suggestions?propertyId&categoryAI suggestion list
POST/bff/backoffice/v1/ai/suggestions/{id}/decideDecide a suggestion (accept/reject/modify)
GET/bff/backoffice/v1/alerts?propertyId&filterAlert inbox
POST/bff/backoffice/v1/alerts/{id}/acknowledgeAcknowledge an alert
PUT/bff/backoffice/v1/preferencesUpdate operator preferences
POST/bff/backoffice/v1/devices/{deviceId}/heartbeatDevice heartbeat (every 60 s)
GET/bff/backoffice/v1/sync/cursor?deviceIdRead device's last-known sync cursor
POST/bff/backoffice/v1/sync/cursorAdvance device's cursor (post-pull)
POST/bff/backoffice/v1/sync/handshakeNegotiate a sync session with sync-service; returns sync-session-token
POST/bff/backoffice/v1/locks/{reservationId}/issue-keyProxy to lock-integration-service.issueKey
POST/bff/backoffice/v1/locks/{reservationId}/revoke-keyProxy to lock-integration-service.revokeKey
GET/bff/backoffice/v1/sse/stream?propertyId&channelsSSE stream for AI suggestions, alerts, dashboard refresh hints
POST/bff/backoffice/v1/auth/refreshDevice-bound JWT refresh (proxy to iam-service)
POST/bff/backoffice/v1/auth/sign-outSign out device

5. Top events published

melmastoon.bff.backoffice.dashboard.viewed.v1, .workbench.opened.v1, .ai_suggestion.decided.v1, .alert.acknowledged.v1, .operator.activity.v1, .device.heartbeat.v1, .sync.cursor_advanced.v1, .sync.handshake_completed.v1, .session.opened.v1, .session.closed.v1, .lock.action_proxied.v1. All telemetry; never domain.

6. Top events consumed

SubjectPurpose
melmastoon.iam.session.revoked.v1Force re-login on affected devices; broadcast on SSE
melmastoon.iam.user.disabled.v1Tear down sessions for disabled operators
melmastoon.reservation.* (selected)Project into dashboard cache hints
melmastoon.housekeeping.task.created.v1Project into housekeeping board hints
melmastoon.maintenance.workorder.opened.v1Project into maintenance board hints
melmastoon.ai.suggestion.created.v1Update AISuggestionInbox cache
melmastoon.ai.suggestion.invalidated.v1Drop from inbox
melmastoon.alert.raised.v1Insert into AlertInbox; push via SSE
melmastoon.alert.resolved.v1Drop from inbox; push via SSE
melmastoon.theme.published.v1Invalidate any rendered shortcut help (if branded)

Events are projected into Memorystore caches for fast dashboard reads; they are not persisted as the source of truth.

7. Upstream services (composes from)

iam-service, tenant-service, property-service, reservation-service, inventory-service, pricing-service, housekeeping-service, maintenance-service, billing-service, lock-integration-service, ai-orchestrator-service, notification-service (alert metadata), sync-service (handshake + cursor), analytics-service (KPI snapshots).

8. Downstream consumers

The Electron desktop application (@ghasi/app-desktop-backoffice) is the only consumer. The narrow contextBridge API surface (window.melmastoon.*) calls REST endpoints listed in §4 from the renderer through the main process.

9. Non-functional

ConcernTarget
/dashboard p95 (warm)< 600 ms
/dashboard first-byte (chunked SSR)< 200 ms
Workbench p95< 400 ms
Lock-action p95< 800 ms (orchestrator-bounded)
Heartbeat p95< 80 ms
Sync handshake p95< 300 ms
Availability99.9% (vs 99.95% for guest-facing BFFs — desktop tolerates BFF being briefly down via offline mode)
Concurrent operators per property50 (peak)
Devices per tenant200 (peak)

10. Cross-references