EVENT_SCHEMAS — bff-backoffice-service
Sibling: API_CONTRACTS · APPLICATION_LOGIC · DOMAIN_MODEL
Cross-cutting: 04 Event-Driven Architecture · Standards · NAMING
This BFF publishes only telemetry. It owns no domain state and emits no domain events. Subjects live under melmastoon.bff.backoffice.*. All payloads carry a hashed-PII posture (no plain email, phone, or name reaches Pub/Sub).
1. Scope
| Class | Pattern | Source of truth | Posture |
|---|---|---|---|
| Telemetry | melmastoon.bff.backoffice.* | This BFF | Hashed PII; sampling per-subject |
| Cache invalidation (consumed) | melmastoon.<service>.<aggregate>.<verb>.v<n> | Producing service | We do not publish; we only consume |
| Domain events | n/a | We do not publish any | n/a |
2. Event envelope (universal)
Per 04 §3:
{
"envelope": {
"eventId": "evt_01H8YN7QV4D8KZ4F8Y5CK4MV3D",
"subject": "melmastoon.bff.backoffice.dashboard.viewed.v1",
"version": 1,
"occurredAt": "2026-04-23T09:14:22.041Z",
"publishedAt": "2026-04-23T09:14:22.058Z",
"producer": "bff-backoffice-service",
"producerInstance": "bff-bo-asia-south1-7f8d9c-x4z2",
"tenantId": "tnt_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"sessionId": "bos_01H8Y...",
"propertyId": "prop_01H8Y...",
"requestId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"causationId": "evt_01H8YN7QV4D8KZ4F8Y5CK4MV3C",
"correlationId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"schemaUri": "https://schemas.melmastoon.ghasi.io/bff-backoffice/dashboard-viewed/v1.json",
"retentionClass": "operational",
"samplingRate": 0.25,
"appVersion": "1.4.2",
"appPlatform": "win32"
},
"payload": { /* per subject */ }
}
3. Topics + sampling
| Subject | Topic | Sampling | Retention |
|---|---|---|---|
melmastoon.bff.backoffice.session.opened.v1 | melmastoon.bff.backoffice.session | 1.0 | regulated (7y audit) |
melmastoon.bff.backoffice.session.closed.v1 | same | 1.0 | regulated |
melmastoon.bff.backoffice.dashboard.viewed.v1 | melmastoon.bff.backoffice.activity | 0.25 | operational (90 d) |
melmastoon.bff.backoffice.workbench.opened.v1 | same | 0.25 | operational |
melmastoon.bff.backoffice.ai_suggestion.decided.v1 | melmastoon.bff.backoffice.ai | 1.0 | regulated |
melmastoon.bff.backoffice.alert.acknowledged.v1 | melmastoon.bff.backoffice.alerts | 1.0 | audit (365 d) |
melmastoon.bff.backoffice.operator.activity.v1 | melmastoon.bff.backoffice.activity | 0.10 | operational |
melmastoon.bff.backoffice.device.heartbeat.v1 | melmastoon.bff.backoffice.devices | 0.05 | operational (30 d) |
melmastoon.bff.backoffice.sync.cursor_advanced.v1 | melmastoon.bff.backoffice.sync | 1.0 | operational |
melmastoon.bff.backoffice.sync.handshake_completed.v1 | same | 1.0 | operational |
melmastoon.bff.backoffice.lock.action_proxied.v1 | melmastoon.bff.backoffice.locks | 1.0 | regulated (7y) |
Sampling rates are flag-controlled; runtime overrides via bff-backoffice-flags Memorystore key with 30 s refresh.
4. Schema — session.opened.v1
{
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"sessionId": "bos_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyScope": ["prop_01H8Y..."],
"primaryPropertyId": "prop_01H8Y...",
"roles": ["front_desk_supervisor"],
"locale": "ps-AF",
"appVersion": "1.4.2",
"appPlatform": "win32",
"ipHash": "sha256:abc123...",
"fingerprintHash": "sha256:def456...",
"openedAt": "2026-04-23T09:14:22Z"
}
5. Schema — session.closed.v1
{
"sessionId": "bos_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"openedAt": "2026-04-23T09:14:22Z",
"closedAt": "2026-04-23T17:42:01Z",
"reason": "sign_out|expired|revoked|device_replaced|version_upgrade",
"activityCount": 184,
"lockProxyCount": 7,
"mutationProxyCount": 41
}
6. Schema — dashboard.viewed.v1
{
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"operatorRole": "front_desk_supervisor",
"viewedAt": "2026-04-23T09:14:22Z",
"renderMode": "composed|cached",
"partial": false,
"missingWidgets": [],
"latencyMs": 412
}
7. Schema — workbench.opened.v1
{
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"view": "arrivals|departures|in_house|today|housekeeping_board|maintenance_board",
"openedAt": "2026-04-23T09:14:22Z",
"renderMode": "composed|cached",
"itemsRendered": 12,
"latencyMs": 240
}
8. Schema — ai_suggestion.decided.v1
{
"suggestionId": "sg_01H8Y...",
"inboxEntryId": "aim_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"category": "housekeeping_reorder",
"severity": "attention",
"decision": {
"outcome": "accepted|rejected|modified",
"modifiedDelta": null,
"notesPresent": false
},
"decidedAt": "2026-04-23T09:14:22Z",
"msSinceCreated": 3640012,
"provenance": {
"model": "ghasi-ops-v3",
"modelVersion": "3.2.1",
"promptVersion": "hk-reorder-v5",
"modelClass": "cloud",
"signatureFingerprint": "sha256:..."
}
}
notesPresent: true|false because notes can carry guest names; the boolean is sufficient signal for analytics.
9. Schema — alert.acknowledged.v1
{
"alertId": "alt_01H8Y...",
"inboxEntryId": "ali_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"category": "lock_failed|oversold|payment_failed|sync_lag|device_offline|security|other",
"severity": "info|warning|critical",
"raisedAt": "2026-04-23T08:42:14Z",
"acknowledgedAt": "2026-04-23T09:14:22Z",
"msFromRaiseToAck": 1928000,
"notesPresent": true
}
10. Schema — operator.activity.v1
{
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"sessionId": "bos_01H8Y...",
"occurredAt": "2026-04-23T09:14:22Z",
"category": "view|mutation_proxy|lock_action|ai_decision|alert_ack|sync_handshake|preferences_changed|auth|other",
"action": "reservation.check_in",
"resourceRef": { "kind": "reservation", "id": "rsv_01H8Y..." },
"outcome": "success|failure|partial",
"latencyMs": 412,
"errorCode": null
}
Sampling 10% by default; raised to 100% during incident windows for the affected tenant via flag override.
11. Schema — device.heartbeat.v1
{
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"deviceId": "dev_01H8Y...",
"operatorId": "opr_01H8Y...",
"occurredAt": "2026-04-23T09:14:22Z",
"appVersion": "1.4.2",
"appPlatform": "win32",
"networkProfile": "good|flaky|offline-recently",
"memoryRssMb": 412,
"cpuPctOneMin": 6.4,
"outboxHints": [
{ "aggregate": "folio.charge", "count": 2, "oldestAgeSeconds": 42 }
],
"lastSyncCursorPresent": true
}
Sampling 5% (heartbeat is high-frequency).
12. Schema — sync.cursor_advanced.v1
{
"tenantId": "tnt_01H8Y...",
"deviceId": "dev_01H8Y...",
"operatorId": "opr_01H8Y...",
"advanceKind": "pull|push|both",
"cursorBefore": "ck_v1_...",
"cursorAfter": "ck_v1_...",
"advancedAt": "2026-04-23T09:14:22Z",
"msSinceLastAdvance": 84000
}
13. Schema — sync.handshake_completed.v1
{
"tenantId": "tnt_01H8Y...",
"deviceId": "dev_01H8Y...",
"operatorId": "opr_01H8Y...",
"appVersion": "1.4.2",
"capabilities": ["bulk-pull-v1","push-batched-v1","resumable-v1"],
"outcome": "ok|reauth_required|version_blocked",
"latencyMs": 220,
"occurredAt": "2026-04-23T09:14:22Z",
"cursorAccepted": "ck_v1_..."
}
14. Schema — lock.action_proxied.v1
{
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorId": "opr_01H8Y...",
"deviceId": "dev_01H8Y...",
"reservationId": "rsv_01H8Y...",
"vendor": "ttlock|salto|assa-vostio|wiegand-generic",
"action": "issue|revoke|rebind|extend",
"outcome": "success|failure",
"errorCode": null,
"mfaAttestationUsed": false,
"occurredAt": "2026-04-23T09:14:22Z",
"upstreamLatencyMs": 614
}
Lock-action events are 100% sampled and 7-year retained as part of the regulated audit lake.
15. Consumed events (cache invalidation only)
| Subject | Action |
|---|---|
melmastoon.iam.session.revoked.v1 | Drop session; SSE forceLogout |
melmastoon.iam.user.disabled.v1 | Tear down all sessions for operator |
melmastoon.ai.suggestion.created.v1 | Insert into AISuggestionInbox; SSE ai.new |
melmastoon.ai.suggestion.invalidated.v1 | Drop entry; SSE ai.removed |
melmastoon.alert.raised.v1 | Insert into AlertInbox; SSE alerts.new |
melmastoon.alert.resolved.v1 | Drop entry; SSE alerts.removed |
melmastoon.theme.published.v1 | Invalidate keyboard shortcut help (if branded) |
melmastoon.reservation.checked_in.v1 | Invalidate dashboard + arrivals + in-house caches for property |
melmastoon.reservation.checked_out.v1 | Invalidate dashboard + departures + in-house caches |
melmastoon.housekeeping.task.created.v1 | Invalidate housekeeping board cache |
melmastoon.housekeeping.task.transitioned.v1 | Same |
melmastoon.maintenance.workorder.opened.v1 | Invalidate maintenance board cache |
melmastoon.maintenance.workorder.transitioned.v1 | Same |
melmastoon.tenant.operator_role.updated.v1 | Drop OperatorPreferences mirror; refresh on next request |
Inbox dedup by eventId via standard inbox table (24h dedup window).
16. Schema registry
All payload schemas live in @ghasi/event-envelope/schemas/bff-backoffice/<event>/vN.json. CI verifies that every published subject has a registered schema. Pact verifies producer + consumer.
17. Compatibility
- Forward-compatible additions allowed within
vN. - Renames or removals require
vN+1with consumer migration window. - Subject
vNretained for 90 days aftervN+1GA.
18. Dead-letter
Pub/Sub DLQ topics: melmastoon.bff.backoffice.<group>.dlq.v1. DLQ depth alert at 50; SRE inspects payload (sanitized) and replays after fix.
19. PII posture verification
A nightly synthetic PII probe (telemetry-pii.spec.ts) injects fake PII into a test run; the test passes only if no plain-text PII appears in any published event payload. Pepper rotation does not affect publish-side checks (which look for raw values, not hashes).