Skip to main content

SYNC_CONTRACT — theme-config-service

Sibling: DATA_MODEL · APPLICATION_LOGIC · EVENT_SCHEMAS

Platform anchors: docs/architecture/ADR-0003-electron-offline-first-desktop.md · docs/standards/SERVICE_TEMPLATE.md

This document defines how theme-config-service interacts with the Electron desktop client ("Ghasi Melmastoon Operator Console") that some hotel operators run on-property for offline-tolerant operations. Per ADR-0003, only services where on-property staff perform critical workflows offline are first-class participants in the desktop sync layer.


1. Sync classification

QuestionAnswer
Is theme-config-service desktop-replicable?No, with one exception.
Replication strategyRead-only mirror of the active published bundle for the property's theme.
Authoring offline?No. All authoring (CRUD, publish, rollback) is cloud-only.
Conflict policyServer wins by definition — desktop never writes back to the service.

Why: theme authoring is a deliberate, low-frequency activity performed by the brand owner / GM on the cloud backoffice; an offline operator does not need to edit theming. What an offline operator does need is for the booking flow embedded in the Electron operator console (e.g. walk-in reservation entry) to render correctly with the property's brand even when the LAN is partitioned from the WAN.

So we replicate exactly one read-model: the property's currently-published theme bundle.


2. The replicated artefact: local_theme_bundle

2.1 SQLite schema (Electron-side)

CREATE TABLE local_theme_bundle (
id TEXT PRIMARY KEY CHECK (id = 'singleton'),
theme_id TEXT NOT NULL,
theme_version_id TEXT NOT NULL,
publication_id TEXT NOT NULL,
bundle_sha256 TEXT NOT NULL,
bundle_size_bytes INTEGER NOT NULL,
bundle_json BLOB NOT NULL, -- gzipped JSON, identical to CDN payload
email_theme_json BLOB NOT NULL, -- gzipped JSON
default_locale TEXT NOT NULL,
enabled_locales TEXT NOT NULL, -- JSON array
fetched_at TEXT NOT NULL, -- RFC3339
origin TEXT NOT NULL CHECK (origin IN ('cdn','origin','provisioning')),
schema_version INTEGER NOT NULL DEFAULT 1
);

The id = 'singleton' row is overwritten on every successful sync — there is no history on the desktop because there is no offline editing to reconcile.

2.2 Mirror semantics

  • Source of truth: the cloud theme_publications row marked is_active=true for the property's theme.
  • Granularity: one bundle per Electron install; chains running multiple properties from one console use propertyId selection in the console UI to switch between bundles (each persisted as a row keyed by local_theme_bundle.theme_id — the schema above is shown for the single-property case; the multi-property variant uses theme_id as the PK and removes the 'singleton' check).
  • Invalidation: triggered by either (a) push notification through the desktop sync channel carrying melmastoon.theme.published.v1, or (b) periodic poll every 15 minutes when the channel is up; or (c) on-demand "Refresh branding" menu action.

3. Sync channels

3.1 Push (preferred)

The desktop sync gateway (desktop-sync-service, separate platform service per ADR-0003) maintains a per-device WebSocket. Theme events are routed through it as small notifications:

{
"kind": "theme_publication_changed",
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"publicationId": "thp_01J...",
"bundleSha256": "9f2c8e...",
"bundleUrl": "https://cdn.melmastoon.app/themes/thm_01J.../published.json",
"occurredAt": "2026-04-23T10:14:55.211Z"
}

The desktop client compares bundleSha256 to its local row and pulls only on mismatch.

3.2 Pull (fallback)

When the WebSocket is down or stale, the client polls:

GET /api/v1/desktop-sync/themes/{themeId}/active
If-None-Match: "<localBundleSha256>"
Authorization: Bearer <device-jwt>

Response 200

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"publicationId": "thp_01J...",
"bundleSha256": "9f2c8e...",
"bundleUrl": "https://cdn.melmastoon.app/themes/thm_01J.../published.json",
"emailThemeUrl": "https://cdn.melmastoon.app/themes/thm_01J.../email.json",
"publishedAt": "2026-04-23T10:14:55.211Z"
}

Response 304 when the SHA matches.

3.3 First-run / re-install bootstrap

On device pairing, the operator console requests the bundle directly from origin:

POST /api/v1/desktop-sync/themes/bootstrap
Authorization: Bearer <pairing-jwt>
Body: { "themeId": "thm_01J...", "deviceId": "dvc_01J..." }

Returns the same payload as 3.2 plus the gzipped bundle bytes inline (so the device does not need internet routing to the CDN host on first run; useful for hotels where IT first whitelists the API origin).


4. Conflict policy

Because the desktop never writes back to theme-config-service, there are no aggregate-level conflicts. The only conflicts are:

ScenarioResolution
Local bundle is older than serverReplace local bundle with server bundle. Server wins, always.
Local bundle is newer than server (impossible without manual file tampering)Treat as corruption: wipe local bundle, refetch via 3.2; raise a desktop diagnostic event desktop.bundle.tamper_suspected.v1.
Bundle download partial / SHA mismatchDiscard download; retry with exponential backoff (max 5); on persistent failure, keep prior bundle and surface a banner "Branding may be outdated".
Theme deleted on serverServer returns 410 Gone; desktop deletes the local row and falls back to the platform default scaffold bundle shipped with the installer.

There is no conflict resolution UI because there is nothing to merge.


5. Security

  • The device JWT used by 3.2 / 3.3 is bound to a deviceId and a single tenant + property scope. It carries no theme:author permissions; it can only read the active published bundle.
  • Bundles are public anyway (they are served via CDN to anonymous booking-flow visitors). No PII is in the bundle.
  • The email_theme_json is also non-sensitive (it carries presentation tokens, not content). Email rendering still requires a server round-trip per the notification-service contract; the desktop only uses email_theme_json for in-app preview.
  • Pairing happens through the platform device-pairing flow (out of scope here); see iam-service documentation.

6. Bandwidth & freshness budgets

MetricTarget
Bundle size on disk≤ 60 KB gzipped (vs the 40 KB CDN budget; the local store keeps a small index header + the email-theme bundle)
Sync round-trip (push → SHA check → fetch)p95 ≤ 3 s on 1 Mbps DSL
Time-to-freshness after cloud publish on a healthy devicep95 ≤ 30 s
Time-to-freshness after a 1-hour WAN partition≤ 1 minute after recovery
Polling interval (fallback)15 minutes; 5 minutes after a known-pending publish (server pushes a theme_publication_pending advisory)

7. Observability on the desktop

The Electron client emits the following diagnostics to desktop-sync-service (which forwards to the platform):

EventWhen
desktop.theme.bundle.installed.v1After a successful sync (carries from/to SHA)
desktop.theme.bundle.refresh_failed.v1After exhausting retries
desktop.theme.bundle.stale.v1When fetched_at is older than 24 h and a sync was attempted
desktop.theme.bundle.tamper_suspected.v1When local SHA does not match recomputed SHA of stored bytes

These show up in the platform observability stack alongside the cloud-side theme.published.v1 and theme.cdn_cache_invalidated.v1 events for end-to-end traceability of "did the property actually receive the rebrand?".


8. Out of scope

  • Authoring offline. Trying to author offline is not supported; the backoffice console is web-only and requires connectivity.
  • Editing the bundle on the device. The bundle is read-only at runtime.
  • Per-device theming. Phase 2 chain branding is property-scoped, not device-scoped.

9. References