Skip to main content

SYNC_CONTRACT — reporting-service

Sibling: DATA_MODEL · APPLICATION_LOGIC · SECURITY_MODEL · platform contract: docs/architecture/ADR-0003 Electron offline-first desktop

The Electron backoffice keeps a read-mostly cache of recent reports for offline review. reporting-service is not an offline-write surface: report runs are always issued by the cloud worker pool. Desktop "Run now" requests are queued locally and sent through bff-backoffice-service when connectivity returns; if offline at request time, the desktop indicates "Will run when online" rather than rendering locally.


1. Replication scope (cloud → desktop)

Per device/user, scoped to the active property unless the operator has multi-property access.

AggregateReplicated rowsConflict policyReason
ReportAll non-archived reports owned by the tenant for properties the user can accessserver_authoritativeCatalog data; users browse
ReportTemplate (lite projection)Templates referenced by replicated Reports, latest published version onlyserver_authoritativeUsed to render filter chooser
ReportRunLast 30 runs per replicated report; plus every run with requested_by = user in last 90 dappend_onlyHistory view
ExportArtifactMetadata for replicated runs (artifact rows, not binary)append_onlyUsed to show "Open"/"Download"
ReportSubscriptionAll subscriptions where recipient_user_id = current user or recipient_device_id = current devicelww+diffUser-managed, can be paused offline
ReportFilterAll scope='user' rows where owner_id = current user plus scope='tenant' for tenantlww+diffSaved filter sets
ReportSchedule (lite)Schedules attached to replicated reports (read-only fields: cron, status, next fire)server_authoritativeView-only on desktop

1.1 Binary artifacts

  • Artifacts ≤ 5 MB with format ∈ {pdf, csv} and retention_class != regulatory_10y_objectlock are prefetched to the desktop sync cache (<userData>/melmastoon/cache/reports/<runId>/) when the run completes.
  • Artifacts > 5 MB or XLSX or regulatory: metadata only; "Open" triggers an on-demand signed-URL fetch via GET /api/v1/reports/runs/{id}/artifacts/{aid}/download proxied through bff-backoffice-service.
  • Cache eviction LRU at 1 GB per device (configurable in desktop settings).

2. Conflict policies

  • server_authoritative — desktop never proposes writes; replication overwrites local rows at the next pull.
  • append_only — rows arrive in event order; desktop must not alter them. Local view sorts by queuedAt DESC.
  • lww+diff — last writer wins per field, with a per-row lastModifiedAt from the cloud. Desktop tracks per-field dirty bits; on push, only dirty fields are submitted with If-Match: <local etag> and the latest server version. Server merges accepted fields and rejects (409 MELMASTOON.REPORTING.CONCURRENT_MODIFICATION) when both sides changed the same field; desktop discards local change after the next pull.

The only lww+diff writes the desktop ever issues for reporting-service are:

  • report_subscriptions.status (active ↔ paused) for owner-equals-current-user rows.
  • report_filters.{name, filters} for scope='user' AND owner_id=current user rows.

All other "writes" (Run now, Schedule create/update, Subscription create) are deferred commands: the desktop stores them in its outbound queue and replays them as REST calls through bff-backoffice-service when online; they are never applied to local state until echoed back through the pull.


3. Pull contract

Desktop sync engine calls sync-service (the central orchestrator), which fans out to per-service pull endpoints. reporting-service exposes:

GET /internal/sync/pull
Headers:
X-Tenant-Id: tnt_…
X-User-Id: usr_…
X-Device-Id: dev_…
X-Cursor: <opaque cursor returned by previous pull, omit for cold start>
X-Page-Size: 500 (default; max 2000)

200 OK
{
"items": [
{ "kind": "Report", "op": "upsert", "row": { … }, "version": 7 },
{ "kind": "ReportRun", "op": "append", "row": { … }, "version": 1 },
{ "kind": "ExportArtifact", "op": "append", "row": { … }, "version": 1 },
{ "kind": "ReportSubscription", "op": "upsert", "row": { … }, "version": 4 },
{ "kind": "ReportFilter", "op": "upsert", "row": { … }, "version": 2 },
{ "kind": "Report", "op": "tombstone", "id": "rep_…" }
],
"nextCursor": "…",
"drained": false
}

Cursor encodes (kind, lastSeenLogPosition, snapshotEpoch) where snapshotEpoch increments any time the user's property scope changes (forces a cold restart for safety). Cursors are signed (HMAC + service key) so the server can detect tampering.


4. Push contract

Allowed pushes (POST batches) per the lww+diff table above:

POST /internal/sync/push
Headers:
X-Tenant-Id, X-User-Id, X-Device-Id
Body:
{
"ops": [
{
"kind": "ReportSubscription",
"id": "sub_01H…",
"expectedVersion": 4,
"patch": { "status": "paused" }
},
{
"kind": "ReportFilter",
"id": "flt_01H…",
"expectedVersion": 1,
"patch": { "name": "AF tax weekly", "filters": { … } }
}
]
}

200 OK
{
"results": [
{ "id": "sub_01H…", "ok": true, "version": 5 },
{ "id": "flt_01H…", "ok": false, "code": "MELMASTOON.REPORTING.CONCURRENT_MODIFICATION", "serverVersion": 2 }
]
}

5. Privacy on device

  • report_subscriptions.recipient_email_enc is never replicated. Desktop only sees recipient_email_hash masked as ***@<domain> for display.
  • regulatory_submissions rows are excluded from desktop replication (PII-heavy guest registration). They appear only in the cloud BFF UI when staff opens the regulatory inbox.
  • ReportRun.resolvedFilters is replicated as-is (no PII inside resolved filters by template constraint). Templates flagged regulatory: true are still replicated as catalog (so staff can view) but their runs do not prefetch artifact binaries.

6. Bandwidth & quotas

MetricBudget
Cold start per device≤ 2 MB metadata
Steady-state hourly delta≤ 200 KB
Artifact prefetch (PDF)gated by 1 GB cache cap, LRU
Push batch size≤ 50 ops
Pull page size500 default, 2000 max

Sync engine rate-limits pulls to once per 30 s when foregrounded, once per 5 min when minimized. WebSocket fan-out from sync-service triggers an immediate pull on report.completed.v1 for the current user's subscribed reports.


7. Failure modes

FailureBehaviour
Desktop offline at "Run now" clickCommand queued locally; UI shows "Will run when online"; replays after connectivity check
Push 409UI surfaces gentle merge prompt for filters/subscriptions; user re-applies or discards
Pull cursor invalidated (snapshotEpoch advanced)Desktop drops local table for that aggregate and cold-starts that kind
Artifact GCS fetch 403 (revoked access)Desktop deletes cached binary; sets row localArtifactStatus='revoked'; user re-pulls signed URL on next click

Cross-references: docs/architecture/ADR-0003, DATA_MODEL §3.7, SECURITY_MODEL §5.