SYNC_CONTRACT — bff-consumer-service
Sibling: SERVICE_OVERVIEW · DATA_MODEL
Cross-cutting: 02 Enterprise Architecture §8 · ADR-0003 Electron Offline-First
Important.
bff-consumer-serviceis never consumed by the Electron desktop app. The desktop is the authenticated staff backoffice; this BFF is the anonymous consumer-meta surface. No aggregate owned by this service is replicated to any client SQLite store. This document records that decision explicitly so the platform sync engine and audit tooling can confirm the BFF is out of scope.
1. Replication scope: NONE
| Aggregate owned | Replicated to desktop SQLite? | Conflict policy |
|---|---|---|
GuestSession | no | n/a (cookie-bound, server-side only) |
SearchSession | no | n/a (TTL 1 h, regenerated on demand) |
Wishlist (anonymous) | no | n/a (Phase 2 may sync to authenticated user, but not to desktop) |
BookingHandoff | no | n/a (single-use, server-side only) |
MetaPageView | no | n/a (telemetry only) |
ConversionFunnelEvent | no | n/a (telemetry only) |
BotScore | no | n/a (security audit, server-side only) |
The platform sync registry (@ghasi/sync-protocol/registry.ts) must not include any bff-consumer.* aggregate. CI fails if a row is added.
2. Why this BFF is not in the sync graph
- No staff workflow on the consumer surface. The Electron desktop is for hotel staff. Anonymous guests never use the desktop. The shape this BFF returns has no relevance to a check-in board, housekeeping queue, or folio screen.
- No tenant scope. The desktop sync protocol is keyed on
(tenantId, userId, deviceId, scope); this BFF emits cross-tenant view-models that don't fit a per-tenant cursor. - No durable shared state. Caches and sessions are short-lived ergonomics. The only durable rows we own (handoff log, wishlist, telemetry, bot score) belong on the cloud only and have explicit retention policies.
- Trust boundary mismatch. The desktop's local DB is encrypted at rest with a device-bound key derived through
iam-service. There is no anonymous-equivalent root-of-trust for the consumer surface.
3. Cross-BFF sync interaction (handoff handshake)
Although no aggregate is replicated, this BFF does participate in a one-shot handshake with bff-tenant-booking-service over the BookingHandoff token. That handshake is not a sync — it is:
- Stateless verification (HMAC over a canonical signing string).
- Single-use (replay-protected via
handoff_replay_logand theconsumedflag). - Cross-BFF only (never reaches the desktop).
bff-consumer mints ─► signed redirect URL ─► bff-tenant-booking verifies + consumes
│ │
│ │
▼ ▼
handoff_replay_log POST /internal/handoff/{id}/consume
(durable durable replay-guard) (marks consumed=true)
This is documented here for completeness because operators sometimes confuse it with sync; it is not.
4. Future-state sync candidates (Phase 2+)
| Candidate | Trigger | Conflict policy that would apply | Status |
|---|---|---|---|
Authenticated Wishlist (per iam-service user) | Phase 2 consumer accounts | lww+diff by addedAt | Deferred; design lives in services/bff-consumer-service/_future/wishlist-auth-sync.md |
| Per-user search history | Phase 2 | lww+diff | Deferred |
Even when Phase 2 lands, none of these will sync to the Electron desktop; they will sync between consumer mobile and consumer web via the iam-service user account.
5. Compliance with the platform sync registry
The @ghasi/sync-protocol/registry.ts file enumerates every aggregate the platform replicates. The CI check sync-registry-allowlist.spec.ts asserts:
import { syncRegistry } from '@ghasi/sync-protocol/registry';
it('does not include bff-consumer aggregates', () => {
for (const entry of syncRegistry) {
expect(entry.serviceName).not.toBe('bff-consumer-service');
}
});
This test runs against every PR in the documentation repo; landing this bundle does not require any registry change.
6. Operational note for SRE
If the platform's per-aggregate conflict-policy matrix in 02 §8.2 is regenerated from per-service SYNC_CONTRACT.mds, this service contributes zero rows. The matrix-builder script must filter on replicatedAggregates.length > 0.