SERVICE_OVERVIEW — bff-backoffice-service
Sibling: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · SYNC_CONTRACT · DEPLOYMENT_TOPOLOGY
Cross-cutting: 03-microservices/bff-backoffice-service · ADR-0003 Electron offline-first desktop · 02 Enterprise Architecture · §8 Sync & Offline
1. One-line purpose
A thin, stateless orchestration layer between the Electron desktop backoffice application and the platform's domain services. It composes dashboards, proxies authenticated mutations, brokers sync handshakes, multiplexes AI suggestion + alert streams, manages device-bound session lifecycle, and emits operator-activity telemetry — without owning domain state.
2. Problem it solves
The Electron desktop client is offline-first: when the network is healthy, it talks to the cloud; when the network is unreliable (the design assumption — see ADR-0003 §5), it falls back to local SQLite and queues mutations. The cloud-side surface that the desktop talks to has three jobs:
- Compose what would otherwise be 8–12 parallel cross-service calls per dashboard render — over potentially flaky network — into a single chunked SSR stream that the desktop can paint progressively even with high latency.
- Bridge Electron-specific lifecycle — device-bound JWT refresh with proof-of-possession, narrow
contextBridgeAPI contract on the desktop side, sync cursor handshake, AI suggestion delivery (poll-fallback for hostile networks; SSE preferred), and lock-action proxying with full audit envelope. - Emit operator telemetry without making the desktop assemble the envelope (operator id, device id, tenant id, property id, role, locale, build version, network mode).
These three jobs would otherwise become 8 different per-feature concerns spread across the desktop renderer; centralizing them here keeps the desktop renderer pure-UI and lets all staff workstations share one composition contract.
3. Bounded context
| In scope | Out of scope |
|---|---|
| Composed dashboard payloads | Dashboard authoring |
| Workbench view composition (front desk, housekeeping, maintenance) | Domain logic on rooms, reservations, folios, work orders, key credentials |
| Operator session lifecycle (open/refresh/close) | Authentication, password reset, MFA setup (lives in iam-service) |
| AI suggestion fetch + decision proxy + audit | AI inference, prompt assembly, model selection (lives in ai-orchestrator-service) |
| Alert inbox + ack proxy | Alert authoring, threshold evaluation (lives in notification-service) |
| Lock-action proxy with audit envelope | Lock-vendor protocols, lock state (lives in lock-integration-service) |
| Device heartbeat + activity ledger | Device provisioning, MDM (lives in iam-service device registry) |
| Sync handshake + cursor cache | Bulk sync pull/push protocol (lives in sync-service) |
| Operator preferences cache + mirror | Preferences source-of-truth (tenant-service.operatorPreferences) |
| Telemetry envelope assembly | Domain events |
4. Aggregates owned
Strict definition: aggregates owned by this BFF are session and projection types only. The BFF stores no domain data.
| Aggregate | Lifecycle | Storage |
|---|---|---|
BackofficeSession | open on auth → idle → refreshed → closed | Memorystore (sessions tier), 12h TTL |
DashboardSnapshot | per-render projection | Memorystore (cache tier), 30s TTL |
WorkbenchView | per-render projection per feature | Memorystore (cache tier), 15s TTL |
AISuggestionInbox | mirror of orchestrator inbox; ack on decision | Memorystore + Postgres ack log |
AlertInbox | mirror of notification inbox; ack on user action | Memorystore + Postgres ack log |
OperatorActivity | append-only log of operator actions for telemetry | Postgres ledger, 90d retention |
DeviceSyncStatus | per-device sync cursor cache + heartbeat | Postgres + Memorystore |
OperatorPreferences | read-through cache over tenant-service preferences | Memorystore + Postgres mirror, 30d retention |
KeyboardShortcutMap | server-rendered map for desktop | Static + Postgres overrides |
OfflineActionQueueHint | informational hint about what desktop has queued | Postgres, 7d retention |
OfflineActionQueueHint is the most subtle: the desktop owns the canonical outbound queue. The hint here is so we can show "you have N pending actions" on a second device for the same operator without the hint device needing direct connection to the primary device's local SQLite. The hint is best-effort and never authoritative.
5. Responsibilities
| # | Responsibility | Notes |
|---|---|---|
| 1 | Resolve operator + tenant + property + role from device-bound JWT | Single layer; downstream calls all carry resolved context |
| 2 | Compose dashboard payload | Per-widget independent fanout with deadlines + skeletons |
| 3 | Compose workbench view payloads | Per-board: front-desk grid, housekeeping board, maintenance board, today panel, arrivals/departures/in-house |
| 4 | Proxy AI suggestion fetches | Orchestrator-bounded; per-suggestion category filter |
| 5 | Proxy AI suggestion decisions | Decision logged here for audit; orchestrator notified async |
| 6 | Proxy alert acknowledgments | Notification service is source-of-truth |
| 7 | Sync handshake | Cursor handshake with sync-service; mints sync-session-token; brokers per-device cursor |
| 8 | Lock-action proxy | Adds operator_id + device_id + mfa_attestation envelope to lock-integration-service calls |
| 9 | Operator preferences read/write | Cache + mirror; source-of-truth = tenant-service |
| 10 | Device heartbeat + activity telemetry | Heartbeat every 60 s; activity events on every meaningful click |
| 11 | SSE multiplexer | One SSE per device; channels: ai, alerts, dashboard-refresh-hints, session |
| 12 | Force-logout broadcast | On iam.session.revoked.v1 for affected device |
| 13 | Domain mutation proxy with audit envelope | POST /reservations/{id}/check-in, etc., proxied to domain services with idem keys |
| 14 | Idempotency safety | All mutations carry X-Idempotency-Key; we de-dupe in Postgres |
| 15 | Locale / theme delivery | Operator UI locale + tenant theme bundle URL on bootstrap |
| 16 | Themed error responses | All errors carry localized userMessage + MELMASTOON.* code |
6. Out of scope (explicit)
- Domain logic of any kind
- Direct DB access to domain data
- Authentication primitives
- Sync pull/push payload assembly (lives in
sync-service) - AI inference (lives in
ai-orchestrator-servicecloud + ONNX Runtime on desktop) - Lock vendor SDKs (lives in
lock-integration-service) - Receipt printing, USB/serial peripheral access (lives in Electron main process)
- Multi-tenant routing across tenants (one operator session = one tenant)
7. Upstream interactions (composes from)
| Service | Calls |
|---|---|
iam-service | Device-bound refresh, session validate, force-logout broadcast |
tenant-service | Tenant profile, operator role + property scope, preferences mirror |
property-service | Property metadata, room types, photos for dashboard tiles |
reservation-service | Today's arrivals/departures/in-house, reservation reads, mutation proxies |
inventory-service | Occupancy KPI, dashboard tiles |
pricing-service | Rate snapshot tiles for dashboard |
housekeeping-service | Board view, task lists |
maintenance-service | Board view, work order list |
billing-service | Folio summary, invoice list |
lock-integration-service | Issue/revoke key proxy with audit envelope |
ai-orchestrator-service | Suggestion fetch, decision report |
notification-service | Alert inbox, ack proxy |
sync-service | Handshake, cursor read/write |
analytics-service | KPI snapshots (occupancy, ADR, RevPAR) |
8. Downstream consumers
Single consumer: @ghasi/app-desktop-backoffice (Electron). The desktop renderer talks to the BFF only through window.melmastoon.* IPC bridge, which is implemented in the Electron main process and proxies HTTP calls to this BFF.
9. Key architectural decisions
| Decision | Rationale |
|---|---|
| BFF stateless on hot path | Simplifies horizontal scaling; sessions in Memorystore; analytics in Postgres outbox |
| Per-widget independent SSR with skeletons | One slow upstream cannot block the dashboard paint |
| SSE preferred, polling fallback | Hostile networks may not allow long-lived connections; desktop falls back automatically |
| Device-bound JWT with proof-of-possession | Reduces token theft blast radius; per-device revocation possible |
| Sync cursor cached in BFF | Cheap GET for desktop reconnect; canonical authority is sync-service |
| Lock-action via BFF, not direct | Adds audit envelope and ensures MFA attestation is enforced before vendor call |
| Domain mutations proxied with idem keys | Desktop owns idem-key generation; BFF dedupes; safe over flaky links |
| No telemetry in offline mode (desktop) | Desktop emits offline activity to its local outbox; replays via sync; BFF receives those replays via the standard event pipeline |
10. Non-functional requirements
| Concern | Target |
|---|---|
/dashboard p95 (warm) | < 600 ms |
/dashboard first-byte (chunked SSR) | < 200 ms |
| Workbench reads p95 | < 400 ms |
| Heartbeat p95 | < 80 ms |
| Sync handshake p95 | < 300 ms |
| Lock-action proxy p95 | < 800 ms (orchestrator-bounded by lock-integration-service) |
| AI suggestion fetch p95 | < 500 ms |
| SSE first event p95 | < 200 ms |
| Availability | 99.9% (desktop tolerates BFF outage via offline mode) |
| Concurrent operators per property | 50 peak |
| Devices per tenant | 200 peak |
| Force-logout fan-out latency | < 5 s from event publish to SSE delivery |
11. Multi-tenancy
Every request resolves tenantId from the device-bound JWT (subject claim). Cross-tenant access attempts are blocked at:
- JWT subject claim check
- Postgres RLS on every owned table
- Memorystore key namespacing
- Cache key derivation includes
tenantId
Tenant isolation tests: tenant-isolation.spec.ts proves cross-tenant access fails at every layer.
12. Versioning posture
- API:
/bff/backoffice/v1/*. Breaking changes ship at/v2with overlap window. - View-model contracts: provider-side Pact verifies against the desktop's published consumer pacts.
- Event subjects: per NAMING,
vNsuffix on every published subject.
13. Repository layout
Per SERVICE_TEMPLATE:
services/bff-backoffice-service/
src/
domain/ # Session/projection aggregates only
application/ # Orchestrators, view-model composers, use cases
infrastructure/ # Upstream gateways, Memorystore, Postgres, Pub/Sub adapters
presentation/ # NestJS controllers, SSE handlers, OpenAPI
main.ts
test/
unit/
integration/
contract/
drizzle/migrations/
openapi/openapi.yaml
*.md (this bundle)
14. Anti-goals
- Do not implement check-in business rules; orchestrate
reservation-service.checkIn. - Do not maintain per-room state; read from
inventory-service+housekeeping-service. - Do not mint AI prompts; orchestrator owns those.
- Do not sign lock credentials; lock service does that.
- Do not persist domain data anywhere.
15. Cross-links
- API_CONTRACTS — primary deliverable
- DOMAIN_MODEL — session/projection types
- APPLICATION_LOGIC — orchestration patterns
- SYNC_CONTRACT — Electron handshake detail
- SECURITY_MODEL — device-bound auth, audit envelope
- DEPLOYMENT_TOPOLOGY — Cloud Run + Electron auto-update interplay
- FAILURE_MODES — offline + chaos posture