ever-sync-adapter Integration
Bringing the ever-sync model home: sync-health + integrity migration, phased connectors.
Status: Design + first migration shipped (
20260604a_sync_health_and_integrity.sql). The rest is phased. Direction (user, 2026-06-04): bringever-sync-adapterand make the change here, not there. medOS-ultra adopts the model; the productionever-sync-adapterrepo is the reference, left untouched. Read with hospital-decision-twin.md (§6.2.1, §11, OQ-12, OQ-13) and modular-multicountry-deployment.md.
0. Thesis
ever-sync-adapter is the @ever portfolio’s production health-data sync appliance — an Electron edge node live at 15 hospitals, syncing HIS → MOPH Cloud (Thailand’s national exchange). It already solves, in the field, the machinery the Hospital Decision Twin’s “hard half” needs: read-only DB pull from any HIS (Oracle/MySQL/MSSQL/PG), per-mode field mapping, normalization (BE→CE, datetime), FHIR R5 transform, and — crucially — a two-tier sync-stats + integrity model richer than anything in medOS today.
This doc records the decision to adopt that model inside medOS-ultra (not rewrite the adapter, not couple the twin to the adapter’s internals) and ships the first concrete artifact: migration 20260604a.
Two facts make this clean:
- Same ecosystem.
ever-sync-adapter+@ever/sqlcipher-drivershare the@everscope with medOSever-api-*. This is portfolio reuse, not third-party integration. - No coupling exists or is wanted. The adapter is standalone (all-local SQLcipher SQLite, pushes to MOPH). medOS gets the model and a de-identified aggregate, never reaches into a local appliance store.
1. Two directions, one kernel (OQ-13 resolved: the model lives here)
ever-sync-adapter |
medOS services/migration |
|
|---|---|---|
| Direction | outbound HIS → MOPH Cloud | inbound foreign-HIS → medOS |
| Surface | Electron edge appliance (per hospital) | NestJS migration “Box” |
| Connectors | Modes A/B/C (Direct-DB / Inbound-GW / REST) | SQL + EHR adapters, HOSxP/JHCIS packs |
| Pull | read-only (READ-ONLY. Never writes to source) |
read-only SQL adapter (same contract) |
| Transform | field-map → normalize (BE→CE/datetime) → FHIR R5 | field-map → normalize → FHIR R4 |
| Stats | log_jobs (ADR-024/026) + sync_events hash-chain (ADR-060) |
migration_jobs (017) + migration_tenants (021) |
They are two directions of one connector kernel. The decision (OQ-13): medOS-ultra is the canonical home of the shared model; the adapter conforms by emitting an aggregate upward (§3), not by being rewritten. Extracting a shared @ever/* npm kernel for the code is a later, portfolio-level step — explicitly out of scope here.
2. The canonical sync-stats + integrity model (brought here)
Migration 20260604a defines, in the medbase read model:
-
sync_run— one row per atomic sync action. Unifiesever-sync-adapterlog_jobsand medOSmigration_jobsbehind adirectiondiscriminator (inbound|outbound|in_cloud). Carries the atomic-action fields (action_tag,parent_run_id,trigger_source), the full count set (source/extracted/transformed/loaded/error/quarantined), RAGgreen/yellow/red, the ADR-026 crash-watchdog fields (last_progress_at,status='crashed',retry_count), and the multi-country scope (country_code/market_pack_code/tenant_id). -
sync_event— the tamper-evident append-only hash-chain, a faithful port ofever-sync-adaptersync_events(ADR-060). Per-tenant chain,seq_nocontiguous from 1:record_hash = sha256( JSON.stringify({ seqNo, prevHash ?? "", stage, progressPercent, message, createdAt }) ) prev_hash = previous row's record_hashcreated_atis stored as the ISO string that fed the hash, so the digest verifies byte-identically to the edge node. Verification walks the chain and fails on a sequence gap, aprev_hashlinkage break, or a recomputed-hash mismatch (the reference impl’sESA-AUDIT-VERIFY-001/002/003). Append-only — neverUPDATE/DELETE. This integrity dimension is whatmigration_jobslacks and the reason to adopt this model rather than the thinner one.
Both raw tables are RLS-isolated per tenant_id (the 021_migration_multi_tenant pattern).
3. The per-country edge-adapter tier + the aggregate bridge
ever-sync-adapter is the Thailand instance of a general pattern: a per-country national-cloud sync adapter, each a local edge node.
TH: HIS ─[ever-sync-adapter]─▶ MOPH Cloud ┐
PH: HIS ─[edge adapter]──────▶ PhilHealth / DOH │ each LOCAL; raw data
JP: HIS ─[edge adapter]──────▶ MHLW / kaigo ┘ stays in-country
│
│ emits ONLY a de-identified sync-health aggregate (no PHI, no raw rows)
▼
medOS sync_health_aggregate ──▶ twin_readiness_v ──▶ Twin "Data readiness & coverage"
sync_health_aggregate (migration 20260604a) is the bridge target. Per (country × market_pack × connector × facet × payload_family × period) it carries coverage_pct, freshness_at, unsent_backlog, error_rate, runs_total/failed, and the integrity bit hash_chain_verified + hash_chain_last_verified_at. Provenance is de-identified (facility_hash, never hospital_code); there is no PHI and no raw row, so it may cross a tier boundary (twin Invariants 5/6). The twin reads twin_readiness_v; it never touches a local appliance store.
The emit contract (what an edge adapter pushes up)
The adapter already has the inputs (system:health, system:metrics, log_jobs aggregates, the boot verifyHashChain() result). The bridge is a thin, periodic, push-up of a de-identified summary:
// @ever sync-health summary — emitted by each per-country edge adapter,
// upserted into medOS sync_health_aggregate. NO PHI. NO raw rows.
interface SyncHealthSummary {
countryCode: string; // 'TH' | 'PH' | 'JP' | ...
marketPackCode?: string;
connectorCode: string; // e.g. 'ever-sync-adapter:moph'
facilityHash: string; // HMAC(hospcode) — de-identified, NOT hospcode
period: string; // 'YYYY-MM-DD'
byPayloadFamily: Array<{
payloadFamily: '43plus' | 'pc1' | 'fhir' | 'migration';
facet?: string; // demand | capacity | reimbursement | ...
coveragePct: number; // % of expected แฟ้ม/files acknowledged
freshnessAt: string; // last successful push/pull (ISO)
unsentBacklog: number;
errorRate: number;
runsTotal: number;
runsFailed: number;
}>;
integrity: {
hashChainVerified: boolean; // result of the adapter's boot verifyHashChain()
lastVerifiedAt: string; // ISO
};
}
Transport is OQ-12 (a) — the candidates are an export file the operator ships, a signed summary endpoint the adapter posts to, or (where the adapter runs near medOS) a direct sync_run-style roll-up. All three land the same sync_health_aggregate row.
3.1 The emit transport (OQ-12a) — spec
Contract (shipped). The wire shape is SyncHealthSummary in @medos/integration-kit — schemaVersion, countryCode, connectorCode, facilityHash, sourceTier, period, byPayloadFamily[], integrity{}. The same module exports validateSyncHealthSummary() (shape + ranges + a de-identification guard — a FORBIDDEN_KEYS set that rejects any hospcode/cid/hn/vn/patient*/name/rows/… key, and rejects a facilityHash that looks like a bare hospcode). The emit client is SyncHealthService.emit() — validate-then-POST, fails closed (an invalid summary is never sent), with the HMAC signer injected so the same client runs from the Electron edge adapter (Node crypto) and in-browser (Web Crypto).
Primary path — signed POST. The adapter POSTs the JSON body to the medOS ingest-sync-health edge function with a detached HMAC-SHA256 signature over the exact bytes:
POST {SUPABASE_URL}/functions/v1/ingest-sync-health
Content-Type: application/json
X-Sync-Health-Signature: sha256=<hex(HMAC-SHA256(rawBody, per_connector_secret))>
Authorization: Bearer <anon-or-service key> # transport auth; the HMAC is the integrity check
The per-connector secret lives on the connector registry row (coding_connectors.config_json / a sync_connector secret), never in the summary. Rotating it is a registry edit.
Fallback — export file (air-gapped sites). Where a hospital server cannot reach the medOS endpoint, the adapter writes the same signed JSON to a file; an operator uploads it on an admin screen, which calls the same edge function. Identical validation + signature path — only the carrier differs (mirrors the adapter’s own Mode-A/B/C “1 hospital, many transports” stance).
The ingester — ingest-sync-health edge function (Deno, to build).
verify HMAC (constant-time) ──▶ validateSyncHealthSummary (reject on any error, incl. PHI guard)
└─reject 401/422─┘ │ ok
▼
idempotent UPSERT sync_health_aggregate ON CONFLICT
(country_code, connector_code, facet, payload_family, period, facility_hash) DO UPDATE
(service-role write — the frontend never writes the read model) ──▶ 200 { accepted, rows }
One row per (country × connector × facet × payload_family × period × facility); a re-emit for the same period overwrites (UPSERT) — so the transport is idempotent and safe to retry. Default cadence = daily (matches the §6.6 freshness column). The function is the only writer of sync_health_aggregate; the twin reads twin_readiness_v.
Boundary invariants: signature verified before parse; PHI guard rejects identifiers; facility_hash is an HMAC, never a hospcode; an unverified hash-chain (integrity.hashChainVerified === false) is stored, not dropped — the twin surfaces it as a red integrity flag rather than hiding the node.
4. Reconciliation with the existing medOS substrate
20260604a converges, it does not duplicate:
migration_jobs(017) → its rows aresync_runwithdirection='inbound'. New code writessync_run;migration_jobsis read-compatible during transition (back-test the projection before cutover).migration_tenants(021) → stays the country/tenant registry (country_codeISO-2, RLS viaapp.tenant_id).sync_run/sync_eventreuse the sametenant_idkey — no second tenancy model.coding_connectors(041) /migration_connector_manifests→ remain the connector registries;sync_run.connector_codereferences them. No third registry.
The twin doc’s twin_connectors/twin_ingestion_runs are dropped in favour of twin_readiness_v over sync_health_aggregate (already recorded in hospital-decision-twin.md §11).
5. What shipped in 20260604a
sync_run · sync_event (hash-chain) · sync_health_aggregate · twin_readiness_v. Additive, idempotent (IF NOT EXISTS), RLS on the two raw tables. Manual-deploy (medbase migrations are not auto-applied — see root CLAUDE.md deployment table).
6. Non-goals & boundaries
- The
ever-sync-adapterrepo is not modified. It is the reference; its.agents/AGENTS.mdis its canonical contract and it carries strict TDD/ADR/HOTFIX discipline. Any change there is its own scoped effort, gated on the user. - No shared-kernel npm package yet. Extracting the read-only-DB + field-map + normalize + FHIR + logging kernel into
@ever/*is OQ-13’s larger arm; deferred. - No PHI crosses a tier. Only
sync_health_aggregate(de-identified) leaves a node. - No twin reach-in. The twin reads the aggregate/view, never an edge appliance’s SQLite.
7. Invariants
- Adopt-here, not rewrite-there — the model lives in medOS-ultra; the production adapter is untouched.
- Append-only integrity —
sync_eventis never updated or deleted; a chain break is a tamper signal, surfaced (degraded mode), never silently repaired. - Hash verifies byte-identically —
created_atis the exact ISO string fed to the digest; the medOS verifier reproduces the edge node’s hash. - Raw stays local; only de-identified aggregates roll up (twin Invariants 5/6).
- One tenancy + one connector registry — reuse
migration_tenants+coding_connectors; never a parallel model. - Multi-country is data, not code — a new country = a new edge adapter emitting the same
SyncHealthSummary; zero new tables.
8. How to extend
- Add a country → stand up that country’s edge adapter (or point an existing connector at its national cloud); have it emit
SyncHealthSummary. No schema change. - Add a facet → it is just a
facetvalue on the aggregate row; the twin’s readiness screen groups by it. - Adopt the model for an in-cloud connector → write
sync_run/sync_eventwithdirection='in_cloud'; the same verifier + readiness view apply.
9. Open questions
- OQ-12 (a) — bridge transport. Export file vs. signed summary endpoint vs. direct roll-up. Lean: signed endpoint where connectivity allows, export file for air-gapped sites.
- OQ-13 — shared connector kernel. Extract
@ever/*package vs. medOS migration service adoptssync_run/sync_eventvs. leave parallel. Portfolio-level; surface to user. First step (this doc) = shared model in medOS; code-sharing is later. - Back-test before cutover — project existing
migration_jobsrows intosync_runand diff before new code writessync_runas truth.
10. File references
| Thing | Path / symbol |
|---|---|
| This migration | infrastructure/medbase/migrations/20260604a_sync_health_and_integrity.sql |
| Twin consumer | hospital-decision-twin.md §6.2.1 / §11 / OQ-12 / OQ-13 |
| Existing inbound substrate | 017_migration_tables.sql, 021_migration_multi_tenant.sql, 041_coding_connectors.sql |
| Reference impl (sibling repo, untouched) | ever-sync-adapter — log_jobs (ADR-024/026), sync_events + SyncEventRepository (ADR-060), MophCloudClient/Pc1BundlePushClient; .agents/AGENTS.md is its canonical contract |