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.
_statuscan becreated,updated, orsynced_changedis 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:
| Direction | Allowed? | Condition |
|---|---|---|
| Offline β Online | Conditional | Blocked if there are unsynced local changes. This prevents orphaning edits that havenβt been pushed yet. |
| Online β Offline | Always | Online 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:
- Authenticate β Refresh user credentials with the server.
- 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. - Apply β WatermelonDB merges pulled changes into the local database.
- Push β Send local changes (records where
_statusiscreated,updated, ordeleted) to the server via POST. - Persist β Update the peerβs
lastSyncedAttimestamp.
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:
- Establish transport β Retrieve the encrypted AES-GCM session with the hub.
- Pull β Send an RPC query (
sync_pull) with the last sync timestamp. The hub returns an encrypted changeset. - Apply β
applyRemoteChanges()processes pulled records with per-column conflict resolution (see below). - 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). - Mark synced β Set
_status = "synced"on all pushed records and permanently delete soft-deleted records. - Persist β Update the peerβs
lastSyncedAttimestamp.
Conflict Resolution
When the same record has been modified both locally and remotely, the app uses a per-column conflict resolution strategy:
- If the local record is deleted, keep it deleted (donβt resurrect).
- Start with the remote recordβs data (remote wins by default).
- For any column listed in the local recordβs
_changedfield, restore the local value (local wins for those columns). - Preserve the local
id,_status, and_changedmetadata.
Example:
| Field | Local Value | Remote Value | Result |
|---|---|---|---|
| name | Alice | Bob | Bob (remote wins β name not in _changed) |
| phone | 111 | 222 | 111 (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
| Trigger | Description |
|---|---|
| Login | Sync runs automatically after successful authentication. |
| Manual | User taps the βSyncβ button in the app. |
| Force sync | User 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:
- If the user has explicitly selected a peer (
activeSyncPeerId), use that peer. - Otherwise, prefer the first active hub peer.
- 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:
| Field | Description |
|---|---|
peerId | Stable identifier (survives IP changes) |
peerType | cloud_server, sync_hub, or mobile_app |
status | active, revoked, or untrusted |
publicKey | Used for encryption and identity verification |
lastSyncedAt | Timestamp of the last successful sync |
Synced Data Models
The following tables are included in synchronization:
patientsvisitseventsappointmentsclinicsclinic_departmentsusersevent_formsregistration_formspatient_additional_attributespatient_vitalspatient_problemsprescriptionsprescription_itemsdrug_catalogueclinic_inventorydispensing_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, andform_dataare stringified before storage, since WatermelonDB stores them as text. date_of_birthβ Stored as aYYYY-MM-DDstring 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
| State | Description |
|---|---|
idle | No sync in progress. |
fetching | Downloading changes from the remote peer. |
resolving | Processing and applying fetched data locally. |
pushing | Uploading local changes to the remote peer. |
error | Sync 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_atorcreated_atis 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.