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.
| Aggregate | Replicated rows | Conflict policy | Reason |
|---|---|---|---|
Report | All non-archived reports owned by the tenant for properties the user can access | server_authoritative | Catalog data; users browse |
ReportTemplate (lite projection) | Templates referenced by replicated Reports, latest published version only | server_authoritative | Used to render filter chooser |
ReportRun | Last 30 runs per replicated report; plus every run with requested_by = user in last 90 d | append_only | History view |
ExportArtifact | Metadata for replicated runs (artifact rows, not binary) | append_only | Used to show "Open"/"Download" |
ReportSubscription | All subscriptions where recipient_user_id = current user or recipient_device_id = current device | lww+diff | User-managed, can be paused offline |
ReportFilter | All scope='user' rows where owner_id = current user plus scope='tenant' for tenant | lww+diff | Saved filter sets |
ReportSchedule (lite) | Schedules attached to replicated reports (read-only fields: cron, status, next fire) | server_authoritative | View-only on desktop |
1.1 Binary artifacts
- Artifacts ≤ 5 MB with
format ∈ {pdf, csv}andretention_class != regulatory_10y_objectlockare 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}/downloadproxied throughbff-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 byqueuedAt DESC.lww+diff— last writer wins per field, with a per-rowlastModifiedAtfrom the cloud. Desktop tracks per-field dirty bits; on push, only dirty fields are submitted withIf-Match: <local etag>and the latest serverversion. 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}forscope='user' AND owner_id=current userrows.
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_encis never replicated. Desktop only seesrecipient_email_hashmasked as***@<domain>for display.regulatory_submissionsrows are excluded from desktop replication (PII-heavy guest registration). They appear only in the cloud BFF UI when staff opens the regulatory inbox.ReportRun.resolvedFiltersis replicated as-is (no PII inside resolved filters by template constraint). Templates flaggedregulatory: trueare still replicated as catalog (so staff can view) but their runs do not prefetch artifact binaries.
6. Bandwidth & quotas
| Metric | Budget |
|---|---|
| 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 size | 500 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
| Failure | Behaviour |
|---|---|
| Desktop offline at "Run now" click | Command queued locally; UI shows "Will run when online"; replays after connectivity check |
| Push 409 | UI 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.