Hikma Health Hikma Health

Mobile Guides

Data Synchronization

How the offline-first data synchronization process works in the Hikma Health mobile application.

Data Synchronization

Hikma Health mobile uses an offline-first architecture. All clinical data is stored locally on the device using WatermelonDB (backed by SQLite), and synchronization happens explicitly β€” either triggered by the user or automatically on login. The app supports three operational modes: offline with cloud sync, offline with hub sync, and fully online.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    UI Layer                           β”‚
β”‚          (Sync Button, Settings, etc.)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               useSync() Hook                         β”‚
β”‚        (Subscribes to XState sync store)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            syncService.ts (startSync)                β”‚
β”‚     - Detects operation mode (offline/online)        β”‚
β”‚     - Resolves active peer (hub or cloud)            β”‚
β”‚     - Delegates to peerSync.ts                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             peerSync.ts (syncDB)                     β”‚
β”‚       Dispatches to the correct sync strategy        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚                          β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
     β”‚ Cloud Sync  β”‚           β”‚  Hub Sync   β”‚
     β”‚ (HTTPS)     β”‚           β”‚ (Encrypted  β”‚
     β”‚             β”‚           β”‚    RPC)     β”‚
     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
            β”‚                         β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  WatermelonDB       β”‚
              β”‚  (Local SQLite)     β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Operation Modes

Offline Mode (Default)

In offline mode, all reads and writes go through WatermelonDB. The local database is the source of truth, and changes are tracked automatically via WatermelonDB’s _status and _changed fields on each record.

  • _status can be created, updated, or synced
  • _changed is a comma-separated list of column names that have been modified locally since the last sync

Data is synchronized with a remote peer (cloud server or local hub) only when sync is explicitly triggered.

Online Mode

In online mode, all reads and writes bypass WatermelonDB entirely and go directly to the server via RPC calls. This provides real-time data access but requires a constant network connection.

Switching between modes:

DirectionAllowed?Condition
Offline β†’ OnlineConditionalBlocked if there are unsynced local changes. This prevents orphaning edits that haven’t been pushed yet.
Online β†’ OfflineAlwaysOnline mode never writes to WatermelonDB, so there is no local data at risk.

Sync Strategies

The app selects a sync strategy based on the peer type of the active sync target.

Cloud Server Sync

Used when the active peer is a cloud server. This leverages WatermelonDB’s built-in synchronize() function.

Flow:

  1. Authenticate β€” Refresh user credentials with the server.
  2. Pull β€” Fetch remote changes from /api/v2/sync?last_pulled_at=<timestamp>. The server returns a changeset (created, updated, deleted records) and a new timestamp.
  3. Apply β€” WatermelonDB merges pulled changes into the local database.
  4. Push β€” Send local changes (records where _status is created, updated, or deleted) to the server via POST.
  5. Persist β€” Update the peer’s lastSyncedAt timestamp.

Hub Sync

Used when the active peer is a local sync hub (another device on the same network). This uses an encrypted RPC transport with AES-GCM encryption.

Flow:

  1. Establish transport β€” Retrieve the encrypted AES-GCM session with the hub.
  2. Pull β€” Send an RPC query (sync_pull) with the last sync timestamp. The hub returns an encrypted changeset.
  3. Apply β€” applyRemoteChanges() processes pulled records with per-column conflict resolution (see below).
  4. Push β€” Fetch all local changes by querying records with a non-synced _status. Send them to the hub via an encrypted RPC command (sync_push).
  5. Mark synced β€” Set _status = "synced" on all pushed records and permanently delete soft-deleted records.
  6. Persist β€” Update the peer’s lastSyncedAt timestamp.

Conflict Resolution

When the same record has been modified both locally and remotely, the app uses a per-column conflict resolution strategy:

  1. If the local record is deleted, keep it deleted (don’t resurrect).
  2. Start with the remote record’s data (remote wins by default).
  3. For any column listed in the local record’s _changed field, restore the local value (local wins for those columns).
  4. Preserve the local id, _status, and _changed metadata.

Example:

FieldLocal ValueRemote ValueResult
nameAliceBobBob (remote wins β€” name not in _changed)
phone111222111 (local wins β€” phone is in _changed)

This approach minimizes data loss by preserving the user’s in-progress edits while still accepting updates from other devices.

What Triggers Sync

TriggerDescription
LoginSync runs automatically after successful authentication.
ManualUser taps the β€œSync” button in the app.
Force syncUser explicitly initiates an upload or download for a specific time range via forceUpload() / forceDownload().

Peer Resolution

When sync starts, the app determines which peer to sync with:

  1. If the user has explicitly selected a peer (activeSyncPeerId), use that peer.
  2. Otherwise, prefer the first active hub peer.
  3. If no active hub exists, fall back to the first active cloud server.

Peers are stored in the local database with the following key fields:

FieldDescription
peerIdStable identifier (survives IP changes)
peerTypecloud_server, sync_hub, or mobile_app
statusactive, revoked, or untrusted
publicKeyUsed for encryption and identity verification
lastSyncedAtTimestamp of the last successful sync

Synced Data Models

The following tables are included in synchronization:

  • patients
  • visits
  • events
  • appointments
  • clinics
  • clinic_departments
  • users
  • event_forms
  • registration_forms
  • patient_additional_attributes
  • patient_vitals
  • patient_problems
  • prescriptions
  • prescription_items
  • drug_catalogue
  • clinic_inventory
  • dispensing_records

Not synced (device-local only): event_logs, peers, app_config.

Date and Field Handling

During sync, the app normalizes data between the server format and WatermelonDB’s expectations:

  • Timestamps β€” All date fields (created_at, updated_at, prescribed_at, etc.) are converted from server formats (ISO strings, Unix seconds) to millisecond timestamps. Missing or invalid dates fall back to the current time.
  • JSON fields β€” Fields like metadata, form_fields, translations, and form_data are stringified before storage, since WatermelonDB stores them as text.
  • date_of_birth β€” Stored as a YYYY-MM-DD string rather than a timestamp.

Sync State Machine

The sync process is managed by an XState store with the following states:

idle β†’ fetching β†’ resolving β†’ pushing β†’ idle
                      ↓
                    error
StateDescription
idleNo sync in progress.
fetchingDownloading changes from the remote peer.
resolvingProcessing and applying fetched data locally.
pushingUploading local changes to the remote peer.
errorSync failed. Can be cleared to return to idle.

Error Handling

  • Concurrent sync β€” If a sync is already in progress, additional sync requests are silently skipped.
  • Network errors β€” The user is shown a context-aware message:
    • Hub: β€œPlease make sure you are on the same network and Wi-Fi is enabled.”
    • Cloud: β€œPlease make sure you have internet or contact your administrator.”
  • General errors β€” Logged to Sentry for monitoring. The user is notified via a toast message.
  • Force reset β€” Available via useSync().forceReset() as an emergency escape hatch to return the sync state machine to idle.

Force Sync

For recovery or backfill scenarios, users can trigger a force sync that operates on timestamps rather than WatermelonDB’s change tracking:

  • Force Upload β€” Queries all records where updated_at or created_at is after a given timestamp and sends them to the specified peer.
  • Force Download β€” Pulls all records changed since a given timestamp from the specified peer and applies them locally with conflict resolution.