API_CONTRACTS — bff-backoffice-service
Sibling: APPLICATION_LOGIC · EVENT_SCHEMAS · SECURITY_MODEL · SYNC_CONTRACT
Cross-cutting: Standards · NAMING · Standards · ERROR_CODES · 05 API Design · ADR-0003
The primary deliverable for this BFF. The API surface is consumed exclusively by the Electron desktop backoffice application through its narrow contextBridge (window.melmastoon.*). All endpoints are versioned under /bff/backoffice/v1/. All request/response shapes are codified in OpenAPI; the file in openapi/openapi.yaml is the single source of truth and is regenerated on every release.
1. Base URLs
| Environment | Base URL |
|---|---|
| Production | https://backoffice.melmastoon.ghasi.io/api/bff/backoffice/v1 |
| Stage | https://stage-backoffice.melmastoon.ghasi.io/api/bff/backoffice/v1 |
| Dev (per-developer) | https://localhost:8083/api/bff/backoffice/v1 |
| Internal canonical | https://api.melmastoon.ghasi.io/bff/backoffice/v1 (also served, for reverse-proxy traffic) |
2. Authentication
| Stage | Mechanism |
|---|---|
| Sign-in | Desktop POST → iam-service.signIn directly (BFF NOT on critical sign-in path) — issues device-bound JWT (access 15 min) + refresh token (30 d) bound to device |
| Refresh | POST /auth/refresh on this BFF — proxies to iam-service.refresh with proof-of-possession |
| Subsequent | Authorization: Bearer <access-token> on every request; access token carries cnf.jkt (JWK thumbprint) of device key |
| Sign-out | POST /auth/sign-out |
Device-bound JWT (RFC 8705 / DPoP-style):
- Each request carries
Authorization: Bearer <jwt>andDPoP: <signed-pop>headers. - BFF verifies
cnf.jktmatches the DPoP key thumbprint. - Tokens stolen without device key are unusable.
3. Common request headers
| Header | Required | Description |
|---|---|---|
Authorization: Bearer <jwt> | yes (except /health/*) | Device-bound access token |
DPoP: <signed-pop> | yes | Single-use proof-of-possession token |
X-Request-Id | recommended (BFF mints if absent) | ULID; idempotency aid |
X-Idempotency-Key | required on mutations | ULID; 24 h scope |
X-Device-Id | yes | Echoes JWT cnf.kid for cross-check |
X-App-Version | yes | e.g. 1.4.2 |
X-App-Platform | yes | win32 / darwin / linux |
X-Network-Profile | optional | good / flaky / offline-recently (informs telemetry) |
X-Property-Id | required for property-scoped reads | Selected property |
Accept-Language | optional | BCP-47; defaults to operator preferences locale |
traceparent | optional | W3C trace propagation |
Content-Type: application/json | yes (mutations) |
4. Common response headers
| Header | Description |
|---|---|
X-Request-Id | Echoed |
X-Composed-At | ISODateTime when composed (cache age aid) |
| `X-Cache: HIT | MISS |
X-Bff-Version | Build SHA |
Cache-Control: no-store | Default for authenticated endpoints |
traceparent | W3C trace |
Content-Language | Resolved locale |
5. Error envelope
Per ERROR_CODES:
{
"error": {
"code": "MELMASTOON.<DOMAIN>.<CODE>",
"message": "Human-readable for operators (English baseline + localized variant when locale is non-en)",
"userMessage": "Localized message safe to render in UI",
"requestId": "req_01H8YN7QF9RTBRZG4F8Y5CK4MV",
"traceId": "00-...-...-01",
"details": { "fieldErrors": [] },
"retryable": false,
"retryAfterSeconds": null,
"documentationUrl": "https://errors.melmastoon.ghasi.io/MELMASTOON.BFF.BACKOFFICE.SUGGESTION_NOT_FOUND"
}
}
5.1 Common error codes
| Code | HTTP | When |
|---|---|---|
MELMASTOON.IAM.SESSION_REQUIRED | 401 | No / invalid Authorization |
MELMASTOON.IAM.SESSION_EXPIRED | 401 | Access token expired (refresh path) |
MELMASTOON.IAM.DPOP_INVALID | 401 | DPoP proof signature / nonce mismatch |
MELMASTOON.BFF.BACKOFFICE.DEVICE_MISMATCH | 403 | X-Device-Id ≠ JWT cnf.kid |
MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE | 403 | Resource tenant ≠ JWT tenant |
MELMASTOON.IAM.NO_PROPERTY_SCOPE | 403 | Operator has no property scope |
MELMASTOON.BFF.BACKOFFICE.PROPERTY_OUT_OF_SCOPE | 403 | Operator not granted on requested property |
MELMASTOON.BFF.BACKOFFICE.MFA_REQUIRED | 401 | Step-up required |
MELMASTOON.BFF.BACKOFFICE.MFA_INVALID_OR_USED | 401 | Attestation invalid or replayed |
MELMASTOON.BFF.BACKOFFICE.SUGGESTION_NOT_FOUND | 404 | Suggestion id unknown |
MELMASTOON.BFF.BACKOFFICE.DECISION_ALREADY_RECORDED | 409 | Suggestion already decided |
MELMASTOON.BFF.BACKOFFICE.ALERT_ALREADY_ACKED | 409 | Alert already acked |
MELMASTOON.BFF.BACKOFFICE.PREFERENCES_CONFLICT | 412 | Optimistic concurrency |
MELMASTOON.GENERAL.RATE_LIMITED | 429 | Per-device rate limit |
MELMASTOON.BFF.UPSTREAM_UNAVAILABLE | 503 | Circuit open |
MELMASTOON.BFF.UPSTREAM_TIMEOUT | 504 | Per-call deadline exceeded |
MELMASTOON.BFF.SCHEMA_DRIFT | 502 | Upstream payload failed schema parse |
MELMASTOON.BFF.CACHE_UNAVAILABLE | 503 | Memorystore session tier down |
MELMASTOON.GENERAL.PRECONDITION_FAILED | 412 | Idem-key reused with different body |
6. Versioning
- URL major:
/v1/. Breaking changes ship at/v2/with overlap. - Minor changes additive; verified by Pact + OpenAPI diff CI gate.
- Deprecated fields emit
Deprecation+Sunsetheaders.
7. Endpoint catalogue (overview)
| Group | Method | Path |
|---|---|---|
| Auth | POST | /auth/refresh |
| Auth | POST | /auth/sign-out |
| Dashboard | GET | /dashboard |
| Workbench | GET | /today |
| Workbench | GET | /arrivals |
| Workbench | GET | /departures |
| Workbench | GET | /in-house |
| Workbench | GET | /housekeeping/board |
| Workbench | GET | /maintenance/board |
| AI | GET | /ai/suggestions |
| AI | POST | /ai/suggestions/{id}/decide |
| Alerts | GET | /alerts |
| Alerts | POST | /alerts/{id}/acknowledge |
| Preferences | GET | /preferences |
| Preferences | PUT | /preferences |
| Devices | POST | /devices/{deviceId}/heartbeat |
| Sync | GET | /sync/cursor |
| Sync | POST | /sync/cursor |
| Sync | POST | /sync/handshake |
| Locks | POST | /locks/{reservationId}/issue-key |
| Locks | POST | /locks/{reservationId}/revoke-key |
| Mutations | POST | /reservations/{id}/check-in |
| Mutations | POST | /reservations/{id}/check-out |
| Mutations | POST | /reservations/{id}/cancel |
| Mutations | POST | /folios/{id}/charges |
| Mutations | POST | /housekeeping/tasks/{id}/transition |
| Mutations | POST | /maintenance/work-orders/{id}/transition |
| MFA | POST | /auth/mfa/step-up |
| SSE | GET | /sse/stream |
| Health | GET | /health/live |
| Health | GET | /health/ready |
8. POST /auth/refresh
Request
POST /api/bff/backoffice/v1/auth/refresh HTTP/1.1
Host: backoffice.melmastoon.ghasi.io
Content-Type: application/json
DPoP: <jwt-signed-with-device-key>
X-Device-Id: dev_01H8Y...
X-App-Version: 1.4.2
X-App-Platform: win32
{
"refreshToken": "rft_…",
"devicePoP": "<DPoP proof>"
}
200 Response
{
"accessToken": "eyJ...",
"accessTokenExpiresAt": "2026-04-23T09:29:22Z",
"refreshToken": "rft_...",
"refreshTokenExpiresAt": "2026-05-23T09:14:22Z",
"operator": {
"id": "opr_01H8Y...",
"displayName": "Sayed Husain",
"roles": ["front_desk_supervisor"],
"propertyScope": ["prop_01H8Y..."]
},
"tenantId": "tnt_01H8Y..."
}
Errors
401 SESSION_EXPIRED (refresh expired), 401 DPOP_INVALID, 403 IAM.DEVICE_REVOKED, 503 UPSTREAM_UNAVAILABLE.
9. GET /dashboard?propertyId=<id>
Composed snapshot. Per-widget skeletons when partial.
200 Response (full)
{
"id": "dsh_01H8Y...",
"tenantId": "tnt_01H8Y...",
"propertyId": "prop_01H8Y...",
"operatorRole": "front_desk_supervisor",
"composedAt": "2026-04-23T09:14:22Z",
"partial": false,
"widgets": {
"today": { "status": "fresh", "data": { "checkInsExpected": 12, "checkOutsExpected": 9, "noShowsLast24h": 0 } },
"arrivals": { "status": "fresh", "data": { "items": [/* 12 */], "total": 12 } },
"departures": { "status": "fresh", "data": { "items": [/* 9 */], "total": 9 } },
"inHouse": { "status": "fresh", "data": { "items": [/* 41 */], "total": 41 } },
"occupancy": { "status": "fresh", "data": { "occupancyPct": 78.5, "soldRooms": 41, "totalRooms": 52 } },
"revenue": { "status": "fresh", "data": { "todayMinor": 1245000, "currency": "AFN", "ytdRevPar": "USD 38.40" } },
"housekeepingSummary": { "status": "fresh", "data": { "dirty": 18, "cleanInspected": 22, "outOfOrder": 2 } },
"maintenanceSummary": { "status": "fresh", "data": { "open": 4, "inProgress": 2, "blocked": 0 } },
"aiInbox": { "status": "fresh", "data": { "items": [/* 5 */], "total": 5 } },
"alerts": { "status": "fresh", "data": { "items": [/* 3 */], "critical": 0 } },
"syncStatus": { "status": "fresh", "data": { "deviceId": "dev_01H8Y...", "lastSyncAt": "2026-04-23T09:13:48Z", "pendingHints": [{"aggregate":"folio.charge","count":2,"oldestAgeSeconds":42}] } }
},
"staleness": {}
}
200 Response (partial)
When a widget times out:
{
"widgets": {
"ai": { "status": "unavailable" },
"alerts": { "status": "fresh", "data": { /* ... */ } },
/* ... */
},
"partial": true,
"staleness": { "ai": "unavailable" }
}
10. GET /today | /arrivals | /departures | /in-house
Identical envelope; data.items shape per route.
{
"id": "wbv_01H8Y...",
"view": "arrivals",
"composedAt": "2026-04-23T09:14:22Z",
"partial": false,
"data": {
"items": [
{
"reservationId": "rsv_01H8Y...",
"guestName": "Maryam K.", // truncated for telemetry safety
"roomTypeId": "rt_01H8Y...",
"roomNumber": "412",
"scheduledArrival": "2026-04-23T15:00:00Z",
"guestsCount": 2,
"loyaltyTier": "gold",
"specialRequests": ["high_floor","vegetarian_breakfast"],
"stayNights": 3,
"rateAmountMinor": 415000,
"currency": "AFN",
"balanceMinor": 0,
"checkedIn": false
}
// ...
],
"total": 12,
"nextPageToken": null
}
}
11. GET /housekeeping/board?propertyId=<id>
{
"id": "wbv_01H8Y...",
"view": "housekeeping_board",
"composedAt": "2026-04-23T09:14:22Z",
"data": {
"rooms": [
{
"roomId": "rm_01H8Y...",
"roomNumber": "412",
"status": "dirty",
"assignedTo": "opr_01H8Y...",
"estimatedReadyAt": "2026-04-23T11:00:00Z",
"lastUpdatedAt": "2026-04-23T09:00:00Z",
"linenChangeRequired": true,
"guestArriving": "2026-04-23T15:00:00Z"
}
],
"summary": { "dirty": 18, "cleanInspected": 22, "outOfOrder": 2 }
}
}
12. GET /maintenance/board?propertyId=<id>
{
"id": "wbv_01H8Y...",
"view": "maintenance_board",
"composedAt": "2026-04-23T09:14:22Z",
"data": {
"workOrders": [
{
"workOrderId": "wo_01H8Y...",
"title": "Leak in 412 bathroom",
"priority": "high",
"status": "in_progress",
"assignedTo": "opr_01H8Y...",
"openedAt": "2026-04-22T18:14:00Z",
"blocking": true,
"roomId": "rm_01H8Y..."
}
],
"summary": { "open": 4, "inProgress": 2, "blocked": 0 }
}
}
13. GET /ai/suggestions?propertyId=<id>&category=<…>&limit=<n>
{
"items": [
{
"id": "aim_01H8Y...",
"suggestionId": "sg_01H8Y...",
"category": "housekeeping_reorder",
"severity": "attention",
"createdAt": "2026-04-23T08:00:00Z",
"expiresAt": "2026-04-23T18:00:00Z",
"summary": "Reorder housekeeping queue: prioritize rooms 412, 415, 410 (arrivals 15:00).",
"rationaleHint": "Estimated overlap with arrival window > 60 min if current order kept.",
"actionTemplate": {
"kind": "housekeeping.reorder",
"diff": [{ "roomId":"rm_412", "fromPosition": 7, "toPosition": 1 }]
},
"provenance": {
"model": "ghasi-ops-v3",
"modelVersion": "3.2.1",
"promptVersion": "hk-reorder-v5",
"modelClass": "cloud",
"signatureFingerprint": "sha256:..."
}
}
],
"total": 5,
"nextPageToken": null
}
14. POST /ai/suggestions/{id}/decide
Request
POST /api/bff/backoffice/v1/ai/suggestions/aim_01H8Y.../decide
Authorization: Bearer ...
DPoP: ...
X-Idempotency-Key: idem_01H8Y...
X-Device-Id: dev_01H8Y...
Content-Type: application/json
{
"outcome": "accepted",
"modifiedDelta": null,
"notes": "Looks right; applying."
}
200 Response
{
"id": "aim_01H8Y...",
"decision": { "outcome": "accepted", "decidedBy": "opr_01H8Y...", "decidedDeviceId": "dev_01H8Y...", "decidedAt": "2026-04-23T09:14:22Z" }
}
Errors
404 SUGGESTION_NOT_FOUND, 409 DECISION_ALREADY_RECORDED, 412 PRECONDITION_FAILED.
15. GET /alerts?propertyId=<id>&filter=<unack|all|critical>
{
"items": [
{
"id": "ali_01H8Y...",
"alertId": "alt_01H8Y...",
"category": "lock_failed",
"severity": "critical",
"raisedAt": "2026-04-23T08:42:14Z",
"resolvedAt": null,
"acknowledgedAt": null,
"payload": { "reservationId": "rsv_01H8Y...", "vendor":"ttlock","reason":"BLE timeout" }
}
],
"summary": { "critical": 1, "warning": 0, "info": 2 }
}
16. POST /alerts/{id}/acknowledge
POST /api/bff/backoffice/v1/alerts/ali_01H8Y.../acknowledge
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{ "notes": "Investigating with on-site engineer." }
200 returns updated AlertInboxEntry. Errors include 409 ALERT_ALREADY_ACKED.
17. PUT /preferences
Optimistic concurrency.
Request
PUT /api/bff/backoffice/v1/preferences
X-Idempotency-Key: idem_01H8Y...
If-Match: "version-7"
Content-Type: application/json
{
"layout": { "density": "compact", "columnsByPage": { "arrivals": ["guestName","room","arrival","balance"] } },
"notifications": { "sound": true, "popup": true, "aiBadge": true },
"i18n": { "locale": "ps-AF", "firstDayOfWeek": "sat" },
"appearance": { "theme": "dark" }
}
200 Response
Returns updated OperatorPreferences with new version.
Errors
412 PREFERENCES_CONFLICT when If-Match mismatch.
18. POST /devices/{deviceId}/heartbeat
Sent every 60 s by the desktop main process.
{
"occurredAt": "2026-04-23T09:14:22Z",
"appVersion": "1.4.2",
"appPlatform": "win32",
"networkProfile": "good",
"currentPropertyId": "prop_01H8Y...",
"outboxHints": [
{ "aggregate": "folio.charge", "count": 2, "oldestAgeSeconds": 42 },
{ "aggregate": "housekeeping.task.transition", "count": 0, "oldestAgeSeconds": 0 }
],
"memoryRssMb": 412,
"cpuPctOneMin": 6.4,
"lastSyncCursor": "ck_v1_..."
}
204 No Content. Errors: 403 DEVICE_MISMATCH, 429 RATE_LIMITED (one per minute).
19. POST /sync/handshake
Negotiate a sync session; thin proxy to sync-service.handshake.
Request
{
"deviceId": "dev_01H8Y...",
"appVersion": "1.4.2",
"capabilities": ["bulk-pull-v1","push-batched-v1","resumable-v1"],
"lastKnownCursor": "ck_v1_..."
}
200 Response
{
"syncSessionToken": "sst_...",
"expiresAt": "2026-04-23T09:44:22Z",
"pullEndpoint": "https://sync.melmastoon.ghasi.io/sync/v1/pull",
"pushEndpoint": "https://sync.melmastoon.ghasi.io/sync/v1/push",
"cursor": "ck_v1_...",
"maxBatchSize": 500,
"compressionAccepted": ["zstd"]
}
After this call, the desktop talks directly to sync-service. The BFF is not on the bulk transfer path. See SYNC_CONTRACT.
20. GET /sync/cursor?deviceId=<id>
{
"deviceId": "dev_01H8Y...",
"lastCursor": "ck_v1_...",
"lastSyncHandshakeAt": "2026-04-23T09:00:01Z",
"pendingHints": [{"aggregate":"folio.charge","count":2,"oldestAgeSeconds":42}],
"status": "online"
}
21. POST /sync/cursor
Notify BFF of cursor advance after a successful pull/push.
{
"deviceId": "dev_01H8Y...",
"cursor": "ck_v1_...",
"advancedAt": "2026-04-23T09:14:22Z",
"advanceKind": "pull|push|both"
}
204. Emits melmastoon.bff.backoffice.sync.cursor_advanced.v1.
22. POST /locks/{reservationId}/issue-key
Request
POST /api/bff/backoffice/v1/locks/rsv_01H8Y.../issue-key
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"vendor": "ttlock",
"encoderId": "enc_lobby_01",
"validFrom": "2026-04-23T15:00:00Z",
"validUntil": "2026-04-26T11:00:00Z",
"scopes": ["room:412"],
"issueChannel": "front_desk_encoder",
"mfaAttestationToken": null
}
200 Response
{
"keyCredentialId": "kc_01H8Y...",
"vendor": "ttlock",
"encoderResponse": { "writeOk": true, "encoderToken": "enc_..." },
"issuedAt": "2026-04-23T09:14:22Z",
"validFrom": "2026-04-23T15:00:00Z",
"validUntil": "2026-04-26T11:00:00Z"
}
Errors
401 MFA_REQUIRED (per tenant policy), 403 PROPERTY_OUT_OF_SCOPE, 409 RESERVATION.OVERBOOKING_BLOCKED, 502 LOCK.VENDOR_UNAVAILABLE, 504 UPSTREAM_TIMEOUT.
23. POST /locks/{reservationId}/revoke-key
Always requires MFA step-up (/auth/mfa/step-up first).
POST /api/bff/backoffice/v1/locks/rsv_01H8Y.../revoke-key
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"keyCredentialId": "kc_01H8Y...",
"reason": "guest_lost_key|guest_compromised|early_departure|other",
"notes": "Guest reported lost key at front desk.",
"mfaAttestationToken": "mfa_01H8Y..."
}
200 returns revocation status; 401 MFA_INVALID_OR_USED if attestation is bad.
24. POST /reservations/{id}/check-in
Generic mutation proxy. Body shape forwarded to reservation-service.checkIn plus audit envelope.
POST /api/bff/backoffice/v1/reservations/rsv_01H8Y.../check-in
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"actualArrivalAt": "2026-04-23T15:42:00Z",
"roomNumberAssigned": "412",
"guestIdScannedRefs": ["doc_01H8Y..."],
"preAuthMinor": 15000,
"currency": "AFN",
"specialNotes": "Guest requested early breakfast tray."
}
200 returns the checked-in reservation snapshot from the reservation service. Errors are passed through unmodified except for envelope normalization.
25. POST /folios/{id}/charges
POST /api/bff/backoffice/v1/folios/fol_01H8Y.../charges
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"kind": "incidental",
"description": "Mini-bar — 2 sodas, 1 chocolate",
"amountMinor": 32000,
"currency": "AFN",
"category": "fnb",
"taxProfileRef": "tax_default_2026"
}
26. POST /housekeeping/tasks/{id}/transition
POST /api/bff/backoffice/v1/housekeeping/tasks/hkt_01H8Y.../transition
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"to": "clean_inspected",
"inspectorOperatorId": "opr_01H8Y...",
"linenLogRef": "lin_01H8Y..."
}
27. POST /maintenance/work-orders/{id}/transition
POST /api/bff/backoffice/v1/maintenance/work-orders/wo_01H8Y.../transition
X-Idempotency-Key: idem_01H8Y...
Content-Type: application/json
{
"to": "completed",
"resolution": "replaced_p_trap",
"partsUsed": [{ "sku":"P-TRAP-50", "qty": 1 }],
"actualCostMinor": 50000,
"currency": "AFN"
}
28. POST /auth/mfa/step-up
Exchanges a primary auth + a fresh MFA factor for an MfaAttestationToken.
{
"scope": "lock_revoke",
"factor": "totp",
"code": "123456"
}
200 returns:
{
"attestationToken": "mfa_01H8Y...",
"expiresAt": "2026-04-23T09:19:22Z",
"scope": "lock_revoke"
}
29. GET /sse/stream?propertyId=<id>&channels=ai,alerts,dashboard-refresh-hints,session
Server-Sent Events. Per-device single connection; multiplex.
Frame example
event: ai
id: evt_01H8Y...
data: {"kind":"new","payload":{ /* AISuggestionInboxEntry */ }}
event: alerts
id: evt_01H8Y...
data: {"kind":"new","payload":{ /* AlertInboxEntry */ }}
event: dashboard-refresh-hints
id: evt_01H8Y...
data: {"propertyId":"prop_01H8Y...","reason":"reservation.checked_in","invalidate":["dashboard","arrivals","in-house"]}
event: session
id: evt_01H8Y...
data: {"kind":"forceLogout","reason":"iam.session.revoked"}
If SSE not available (proxy strips it), the desktop falls back to polling:
GET /ai/suggestionsevery 60 sGET /alertsevery 30 sGET /dashboardre-render on each manual reload
The polling fallback is selectable via the X-Network-Profile: flaky header or the desktop's preferences.
30. GET /health/live and GET /health/ready
| Path | Purpose |
|---|---|
/health/live | 200 if process running |
/health/ready | 200 if Memorystore + Postgres + 80% of upstream circuits closed |
Open paths; never carry auth.
31. Rate limits
| Scope | Limit | Notes |
|---|---|---|
| Per device per endpoint family | 60 / min for reads, 30 / min for mutations | Token bucket |
/devices/*/heartbeat | 1 / min | Strictly enforced |
/auth/refresh | 5 / 15 min / device | Anti-brute-force |
/locks/* | 30 / hour / device | Sensitive |
/sse/stream | 1 active conn / device | New conn closes prior |
429 carries Retry-After.
32. Pagination
Cursor-based for any list endpoint:
GET /alerts?cursor=<opaque>&limit=50
Response carries nextPageToken. Limit max 100.
33. CORS
Requests originate from app://melmastoon-backoffice (Electron custom URL scheme), not browser origins. CORS allow-list includes only:
app://melmastoon-backofficehttps://stage-backoffice-debug.melmastoon.ghasi.io(debug build)http://localhost:5173(dev renderer)
All others 403.
34. Caching policy
| Endpoint | Server cache | Client cache |
|---|---|---|
/dashboard | 30 s Memorystore | none (always re-fetch on focus) |
| `/today | /arrivals | /departures |
| `/housekeeping/board | /maintenance/board` | 15 s Memorystore |
/ai/suggestions | upstream-bounded | none |
/alerts | upstream-bounded | none |
/preferences | 5 min Memorystore | desktop owns local mirror; refresh on app focus |
/sync/cursor | 30 s Memorystore | desktop reads on reconnect |
35. Compatibility — Electron contextBridge mapping
The desktop maps these endpoints behind window.melmastoon.*:
| Endpoint | Bridge call |
|---|---|
GET /dashboard | window.melmastoon.dashboard.fetch({propertyId}) |
GET /today | window.melmastoon.workbench.today({propertyId}) |
POST /reservations/{id}/check-in | window.melmastoon.reservations.checkIn(id, body) |
POST /locks/{id}/issue-key | window.melmastoon.locks.issueKey(rsv, body) |
POST /sync/handshake | window.melmastoon.sync.handshake(input) |
GET /sse/stream | window.melmastoon.sse.subscribe(channels, onEvent) |
POST /devices/{id}/heartbeat | window.melmastoon.telemetry.heartbeat(payload) |
POST /ai/suggestions/{id}/decide | window.melmastoon.ai.decide(id, decision) |
The bridge layer in the Electron main process attaches Authorization, DPoP, X-Device-Id, X-App-Version, X-App-Platform, X-Network-Profile, and X-Idempotency-Key automatically.
36. OpenAPI
openapi/openapi.yaml is generated from controller decorators. CI fails if the generated file diverges from the committed one without a corresponding PR.