medOS ultra

ever-sync-adapter Integration

Bringing the ever-sync model home: sync-health + integrity migration, phased connectors.

9 min read diagramsUpdated 2026-06-03docs/architecture/ever-sync-adapter-integration.md

Status: Design + first migration shipped (20260604a_sync_health_and_integrity.sql). The rest is phased. Direction (user, 2026-06-04): bring ever-sync-adapter and make the change here, not there. medOS-ultra adopts the model; the production ever-sync-adapter repo 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-driver share the @ever scope with medOS ever-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. Unifies ever-sync-adapter log_jobs and medOS migration_jobs behind a direction discriminator (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), RAG green/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 of ever-sync-adapter sync_events (ADR-060). Per-tenant chain, seq_no contiguous from 1:

    record_hash = sha256( JSON.stringify({
      seqNo, prevHash ?? "", stage, progressPercent, message, createdAt
    }) )
    prev_hash   = previous row's record_hash
    

    created_at is 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, a prev_hash linkage break, or a recomputed-hash mismatch (the reference impl’s ESA-AUDIT-VERIFY-001/002/003). Append-only — never UPDATE/DELETE. This integrity dimension is what migration_jobs lacks 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-kitschemaVersion, 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 are sync_run with direction='inbound'. New code writes sync_run; migration_jobs is read-compatible during transition (back-test the projection before cutover).
  • migration_tenants (021) → stays the country/tenant registry (country_code ISO-2, RLS via app.tenant_id). sync_run/sync_event reuse the same tenant_id key — no second tenancy model.
  • coding_connectors (041) / migration_connector_manifests → remain the connector registries; sync_run.connector_code references 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-adapter repo is not modified. It is the reference; its .agents/AGENTS.md is 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

  1. Adopt-here, not rewrite-there — the model lives in medOS-ultra; the production adapter is untouched.
  2. Append-only integritysync_event is never updated or deleted; a chain break is a tamper signal, surfaced (degraded mode), never silently repaired.
  3. Hash verifies byte-identicallycreated_at is the exact ISO string fed to the digest; the medOS verifier reproduces the edge node’s hash.
  4. Raw stays local; only de-identified aggregates roll up (twin Invariants 5/6).
  5. One tenancy + one connector registry — reuse migration_tenants + coding_connectors; never a parallel model.
  6. 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 facet value on the aggregate row; the twin’s readiness screen groups by it.
  • Adopt the model for an in-cloud connector → write sync_run/sync_event with direction='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 adopts sync_run/sync_event vs. 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_jobs rows into sync_run and diff before new code writes sync_run as 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-adapterlog_jobs (ADR-024/026), sync_events + SyncEventRepository (ADR-060), MophCloudClient/Pc1BundlePushClient; .agents/AGENTS.md is its canonical contract
Ask Anything