Skip to main content

Sync Component

Introduction​

The Sync component is a non-visual SDUI component that enables offline-first capabilities for mobile applications. It defines what data to pre-load and cache locally, how to queue operations (workflow executions, file uploads) when offline, and how to synchronize changes when connectivity is restored.

The Sync component acts as a data layer adapter β€” other SDUI components (datasource, form, button) interact with it transparently, without needing to know whether data comes from the network or local cache.

Key Capabilities​

  • Data pre-loading: Cache configurable datasets for offline access
  • Operation queuing: Persist workflow executions and mutations when offline
  • Background sync: Process queued operations automatically via expo-task-manager
  • Attachment uploads: Queue and upload files in the background with compression
  • Status visibility: Expose sync state for UI indicators and an operations screen
  • Conflict resolution: Last-write-wins strategy for offline changes (see Conflict Resolution)

Design Principles​

  • Declarative: Sync behavior is defined in YAML, consistent with the SDUI pattern
  • Transparent: Consuming components don't distinguish between online and offline
  • Resilient: Queue persists across app restarts and survives app kills
  • Configurable: Each workflow and model can specify its own sync strategy

YAML Structure​

component: sync
name: <syncInstanceName>
props:
models:
- name: <string>
query: <graphql>
variables: <object>
keys: [<string>]
syncInterval: <number>
syncOn: [<trigger>]
maxAge: <number>

queue:
maxRetries: <number>
retryInterval: <number>
retryStrategy: <strategy>
persistQueue: <boolean>
backgroundSync: <boolean>
backgroundInterval: <number>
maxQueueSize: <number>

workflows:
default: <mode>
items:
- workflowId: <string>
mode: <mode>
optimisticResult: <object>
rollbackAction: <action[]>
dependsOn: [<string>]

attachments:
strategy: <strategy>
maxConcurrent: <number>
retryOnFailure: <boolean>
compress:
enabled: <boolean>
quality: <number>
maxDimension: <number>

status:
showIndicator: <boolean>
position: <position>
showPendingCount: <boolean>
notifyOnComplete: <boolean>
notifyOnError: <boolean>

Attribute Description​

Top-Level Props​

AttributeTypeRequiredDescription
modelsarrayNoList of data models to pre-load and cache for offline access
queueobjectNoConfiguration for the offline operation queue
workflowsobjectNoWorkflow execution mode configuration (queue vs optimistic)
attachmentsobjectNoFile upload queue configuration
statusobjectNoSync status UI indicator configuration

Models​

Each model defines a dataset to cache locally for offline access.

AttributeTypeDefaultDescription
namestringβ€”Required. Unique identifier for this model within the sync instance
querystringβ€”Required. GraphQL query string to fetch the data. Must return a single root field containing an items array (or a root-level array) β€” see Model Query Shape
variablesobject{}Template-parsed variables passed to the query. Supports {{ }} expressions β€” global scope only, see Variable Scope
keysstring[]β€”Required. Primary key field(s) for cache identity and deduplication
syncIntervalnumber300Seconds between automatic background refreshes when online. Ignored when syncOn is [manual]
syncOnstring[]["networkReconnect"]Additional triggers for syncing this model
maxAgenumber86400Maximum seconds before cached data is considered stale (default: 24h)
filterstringβ€”Optional default filter applied to cached results
maxItemsnumber1000Maximum number of items to cache for this model

syncOn Triggers​

ValueDescription
appForegroundSync when the app returns to the foreground
networkReconnectSync when network connectivity is restored
manualOnly sync when explicitly triggered via the syncNow/syncModel adapter actions or the refresh action. Must be the only trigger listed; suppresses syncInterval
intervalOnlyOnly sync on the defined syncInterval, no event-based triggers

Model Query Shape​

The engine extracts cached items from the query result by convention: the response must contain a single root field whose value either has an items array (the standard paginated list shape) or is itself an array. Results that do not match this shape cache nothing and log a warning.

Model Variable Scope​

Model variables and the model-level filter are evaluated when the engine syncs in the background β€” timers, reconnect, and app-foreground triggers run with no screen open. Expressions must therefore resolve from the global scope only: organizationId, user.* / currentUser.*, and global variables. Screen- or form-scoped expressions in model definitions are undefined behavior. (Workflow inputs are unaffected β€” they are fully resolved at submit time, while the screen still exists.)

Queue​

Configuration for the offline operation queue that persists and processes mutations/workflows.

AttributeTypeDefaultDescription
maxRetriesnumber5Maximum retry attempts for a failed operation
retryIntervalnumber30Base retry delay in seconds
retryStrategystring"exponential"Backoff strategy: "exponential" or "fixed"
persistQueuebooleantrueWhether the queue persists to device storage (survives app restart)
backgroundSyncbooleantrueWhether to use expo-task-manager for background queue processing. Currently a no-op β€” see note below
backgroundIntervalnumber60Minimum seconds between background sync attempts
Foreground-only sync (current implementation)

True background execution (app killed or backgrounded) requires expo-task-manager, which is not yet enabled in the mobile app. backgroundSync: true is accepted but currently a no-op: the queue processes on network reconnect, app foreground, interval timers, and manual syncNow β€” all while the app is running. Queued operations always survive restarts and process on the next launch.

| maxQueueSize | number | 100 | Maximum pending operations. When the queue is full, new offline operations fail immediately and the action's onError fires |

Operation Identity and Replay Safety​

Every operation is stamped with a client-generated operation ID (GUID) when it enters the queue. Workflow operations send this ID as the executionId of the executeWorkflow call on every attempt, and the server deduplicates on it: if an earlier attempt actually succeeded β€” for example, the request timed out after the server had already accepted it β€” the retry returns the original execution instead of running the workflow again. This is what makes the retry policy above safe: an ambiguous network failure can never double-execute a business operation.

Failure Classification​

Not every failure is retried. The queue classifies errors as:

ClassExamplesBehavior
TransientNetwork errors, timeouts, HTTP 5xxRetried per retryStrategy up to maxRetries, then treated as permanent
PermanentWorkflow validation errors, business rule rejections, HTTP 4xxFails immediately β€” no retries. Fires onSyncFailed, then rollbackAction
AuthenticationExpired or invalid token (HTTP 401)Queue pauses until the user re-authenticates; does not consume retry attempts

Permanently failed operations remain visible in the Sync Operations screen, where the user can inspect, retry, or discard them.

Workflows​

Defines how each workflow action behaves when executed offline.

AttributeTypeDefaultDescription
defaultstring"queue"Default mode for workflows not explicitly listed. Only "queue" may be used as the default β€” "optimistic" requires per-workflow targetModel/targetKey
itemsarray[]Per-workflow configuration overrides

Workflow Item​

AttributeTypeDefaultDescription
workflowIdstringβ€”Required. The workflow identifier to configure
modestringinheritedExecution mode: "queue" or "optimistic"
targetModelstringβ€”Required in optimistic mode. The cached model this workflow updates
targetKeystringβ€”Required in optimistic mode. Template expression, evaluated against the workflow inputs, that resolves the key of the cached record to patch (e.g., "{{ inputs.orderId }}")
optimisticResultobjectβ€”Fields shallow-merged into the targeted cached record when in optimistic mode. Must match the cached record's shape
rollbackActionaction[]β€”Actions executed if the operation fails permanently after all retries
dependsOnstring[]β€”Queue item types that must complete before this workflow executes. upload waits for the attachments referenced in this operation's inputs, not all pending uploads

Execution Modes​

  • queue: The workflow call (workflowId + inputs) is stored in the queue. It executes server-side when connectivity is available. The user receives a "queued" confirmation but no immediate result.
  • optimistic: The engine locates the record in targetModel whose key matches the evaluated targetKey, snapshots it, and shallow-merges optimisticResult into it immediately β€” the user sees the change instantly. The actual workflow executes when connectivity is available. If it fails permanently, the snapshot is restored and rollbackAction runs. If no cached record matches targetKey, the operation falls back to queue mode.

Optimistic mode only updates existing cached records. Workflows that create new entities must use queue mode β€” the server assigns identity and computed fields that the client cannot predict.

Attachments​

Configuration for the file upload queue.

AttributeTypeDefaultDescription
strategystring"background"Upload strategy: "background" (queue for later) or "immediate" (block until done)
maxConcurrentnumber2Maximum simultaneous upload operations
retryOnFailurebooleantrueWhether failed uploads are retried automatically
compress.enabledbooleanfalseWhether to compress images before upload
compress.qualitynumber0.8Image compression quality (0.0–1.0)
compress.maxDimensionnumber2048Maximum width or height in pixels (aspect ratio preserved)

Offline Uploads and Attachment IDs​

Attachment fields opt into the sync queue explicitly via options.syncAdapter:

- component: field
name: photos
props:
type: attachment
options:
syncAdapter: inspectionSync
allowMultiple: true

Attachment identity is client-generated: when a file is selected, the field immediately generates a GUID that becomes the permanent attachmentId β€” whether the upload happens now or hours later. The GUID is returned to the form right away, so workflow inputs can reference it before the file has ever reached the server; no placeholder rewriting is needed. When the queued upload completes, the server stores the attachment record under the same client-supplied GUID.

Because the GUID only becomes resolvable once the file exists server-side, workflows whose inputs reference attachments must declare dependsOn: [upload] so they execute only after their own files have finished uploading.

Server-side, an attachment referenced by GUID before its file has arrived is stored with status PendingUpload; it becomes Active when the queued upload completes, or UploadFailed if the upload fails permanently. Pending attachments are excluded from normal attachment queries, so half-synced files never appear in document listings. The Sync Operations screen surfaces these statuses alongside the queue items.

Status​

Configuration for the sync status UI indicator.

AttributeTypeDefaultDescription
showIndicatorbooleantrueWhether to show a sync status badge/icon
positionstring"header"Where to display the indicator: "top", "bottom", or "header"
showPendingCountbooleantrueWhether to show the count of pending operations as a badge
notifyOnCompletebooleantrueShow notification when all queued operations sync successfully
notifyOnErrorbooleantrueShow notification when an operation fails permanently

Lifecycle and Placement​

A sync instance is registered globally on first render and lives for the rest of the app session β€” it is not tied to the screen that declared it:

  • Place the sync component in the root screen of the module whose data it manages (or in a shared module for global data). It registers when the user first opens that screen.
  • Navigating away does not stop syncing, clear the cache, or pause the queue. The sync engine, queue processor, and background worker are app-wide singletons serving all registered instances.
  • Re-rendering a sync component whose name is already registered updates that instance's configuration in place (idempotent re-registration). Two different sync instances must not share a name.
  • Cached data and queues are namespaced per sync instance (syncName/modelName) and scoped to the current organization. Switching organizations switches to a separate cache namespace; the previous organization's cache and queue are preserved and resume processing when the user switches back.
  • Queued operations survive app restarts (persistQueue: true) and are processed even if the originating module's screen is never reopened.

Adapter Interface​

Other SDUI components interact with the Sync component via the syncAdapter prop. This provides transparent offline access without the consuming component needing to handle online/offline logic.

Usage in DataSource​

- component: datasource
name: vehicleData
props:
name: vehicleData
queries:
- name: getVehicles
query:
syncAdapter: receivingSync
model: vehicles
filter: "trackingNumber:{{ trackingNumber }}"

When a query specifies syncAdapter, the datasource reads that query's result from the local cache instead of the network. If online and the cached data is stale (or was never fetched), fresh data is fetched in the background and re-delivered to the screen automatically when it lands (stale-while-revalidate). The syncAdapter/model/filter combination is a third query source alongside the existing command and workflow forms, and the result is stored under the query name as usual.

Read Result Shape​

A sync-adapter query stores this shape under the query name:

{
"items": [],
"totalCount": 0,
"fetchedAt": "2026-07-03T10:15:00.000Z",
"isStale": false
}

Consuming components bind the item list explicitly β€” e.g. {{ getVehicles.items }} for a collection, or {{ getVehicles.items.0 }} for a single record looked up by filter. fetchedAt is null and isStale is true when the model has never been synced (prime models online first).

Filter Grammar​

The filter string is template-resolved first, then evaluated client-side against the cached items:

  • Terms have the form fieldPath:value; the path supports dot notation (orderStatus.orderStatusName:Received) and splits on the first colon only.
  • Multiple terms combine with AND (uppercase). There is no OR and no comparison operators in this version.
  • Matching uses loose string equality (orderId:42 matches the number 42).
  • A term whose value resolves to empty (missing template variable) matches no items β€” predictable behavior while route params are not yet available.

Usage in Workflow Actions​

- component: button
props:
label:
en-US: Submit
onClick:
- workflow:
workflowId: "6bda41d8-..."
inputs:
data: "{{ form.values }}"
syncAdapter: receivingSync
onQueued:
- notification:
message:
en-US: "Saved offline. Will sync when connected."
variant: info
onSynced:
- notification:
message:
en-US: "Successfully synced to server."
variant: success

Adapter Actions​

The sync adapter exposes the following actions that can be triggered from YAML:

ActionDescription
syncNowForce immediate sync of all models and process queue
syncModelForce sync of a specific model by name
clearCacheClear all cached data for this sync instance
clearQueueDiscard all pending queue operations
retryFailedRetry all permanently failed operations
- component: button
props:
label:
en-US: Refresh Data
onClick:
- syncAction:
adapter: receivingSync
action: syncModel
model: vehicles

New Action Events​

When syncAdapter is specified on a workflow action, additional events become available:

EventDescription
onQueuedFired when the operation is added to the offline queue (offline path)
onSyncedFired when a previously queued operation completes successfully
onSyncFailedFired when a queued operation fails permanently after all retries

Interplay with onSuccess / onError​

The existing onSuccess and onError events of the workflow action keep their current behavior when the device is online. The sync events cover the offline path:

ScenarioEvents fired
Online β€” workflow executes immediatelyonSuccess or onError (unchanged)
Offline β€” operation added to the queue (queue or optimistic mode)onQueued, immediately
Queued operation later syncs successfullyonSynced
Queued operation fails permanently after all retriesonSyncFailed, then rollbackAction (optimistic mode)

onSuccess and onError never fire for queued executions β€” by the time the queue is processed, the screen that initiated the action may no longer be open. Use onQueued for immediate user feedback and onSynced/onSyncFailed (or the status indicator and notifications) for post-sync feedback.

Examples​

Basic: Cache a Single Model​

Pre-load customer contacts for offline access with refresh every 10 minutes:

component: sync
name: contactsSync
props:
models:
- name: customers
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, filter: "contactType:Customer") {
items {
contactId
name
email
phone
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]

Intermediate: Multiple Models with Workflow Queue​

A receiving module that caches vehicles and locations, with offline workflow support:

component: sync
name: receivingSync
props:
models:
- name: vehicles
query: |
query($organizationId: Int!, $filter: String) {
orders(organizationId: $organizationId, filter: $filter, limit: 500) {
items {
orderId
trackingNumber
orderStatus { orderStatusName }
orderCommodities {
commodity { customValues }
}
billToContact { contactId name }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
filter: "orderType:Freight"
keys: [orderId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]

- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items {
warehouseLocationId
name
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600

queue:
maxRetries: 5
retryStrategy: exponential
backgroundSync: true
backgroundInterval: 60

workflows:
default: queue
items:
- workflowId: "6bda41d8-28af-42ac-9975-0f7adc0dc45e"
mode: optimistic
targetModel: vehicles
targetKey: "{{ inputs.orderId }}"
optimisticResult:
orderStatus:
orderStatusName: Received
rollbackAction:
- notification:
message:
en-US: "Receiving failed to sync. Check Sync Operations."
variant: error
dependsOn: [upload]

- workflowId: "2ff3c10b-d2d9-418f-a41a-8834a58abf2e"
mode: queue

Advanced: Full Offline Module with Attachments​

Complete sync configuration for a field inspection module with photo capture and background upload:

component: sync
name: inspectionSync
props:
models:
- name: assignments
query: |
query($organizationId: Int!, $filter: String) {
inspectionAssignments(
organizationId: $organizationId
filter: $filter
) {
items {
assignmentId
location { name address }
scheduledDate
inspectionType
status
notes
}
}
}
variables:
organizationId: "{{ number organizationId }}"
filter: "assignedTo:{{ user.userId }} AND status:Pending"
keys: [assignmentId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]
maxItems: 200

- name: inspectionTemplates
query: |
query($organizationId: Int!) {
inspectionTemplates(organizationId: $organizationId) {
items {
templateId
name
sections { sectionId title fields { fieldId label type required } }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [templateId]
syncOn: [manual]

queue:
maxRetries: 5
retryStrategy: exponential
retryInterval: 30
backgroundSync: true
backgroundInterval: 60
maxQueueSize: 200
persistQueue: true

workflows:
default: queue
items:
- workflowId: "a1b2c3d4-complete-inspection"
mode: optimistic
targetModel: assignments
targetKey: "{{ inputs.assignmentId }}"
optimisticResult:
status: Completed
rollbackAction:
- notification:
message:
en-US: "Inspection submission failed. Your data is saved locally."
variant: warning
dependsOn: [upload]

attachments:
strategy: background
maxConcurrent: 2
retryOnFailure: true
compress:
enabled: true
quality: 0.8
maxDimension: 2048

status:
showIndicator: true
position: header
showPendingCount: true
notifyOnComplete: true
notifyOnError: true

Using the Adapter in a Form​

How other components consume the sync adapter for seamless offline operation:

- component: form
name: inspectionForm
props:
validationSchema: {}
children:
# Load assignment data from cache
- component: datasource
name: assignmentData
props:
name: assignmentData
queries:
- name: getAssignment
query:
syncAdapter: inspectionSync
model: assignments
filter: "assignmentId:{{ assignmentId }}"

# Form fields...
- component: field
name: notes
props:
type: text
label:
en-US: Inspection Notes

# Photo attachment with offline queue
- component: field
name: photos
props:
type: attachment
label:
en-US: Take Photos
options:
syncAdapter: inspectionSync
allowMultiple: true
displayAs: image
parentType: Inspection
parentId: "{{ assignmentId }}"

# Submit with offline support
- component: button
name: submitInspection
props:
label:
en-US: Complete Inspection
options:
variant: primary
onClick:
- validateForm:
validationSchema:
notes:
required:
message: Notes are required
- workflow:
workflowId: "a1b2c3d4-complete-inspection"
inputs:
assignmentId: "{{ assignmentId }}"
notes: "{{ inspectionForm.notes }}"
photos: "{{ inspectionForm.photos }}"
syncAdapter: inspectionSync
onQueued:
- notification:
message:
en-US: "Inspection saved. Will sync when online."
variant: info
- navigateBackOrClose:
onSynced:
- notification:
message:
en-US: "Inspection synced successfully."
variant: success

Global Sync (Shared Module)​

A sync component defined in a shared module, caching data used across all modules:

component: sync
name: globalSync
props:
models:
- name: contacts
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, limit: 1000) {
items { contactId name contactType email phone }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]

- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items { warehouseLocationId name code }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600

- name: users
query: |
query($organizationId: Int!) {
users(organizationId: $organizationId) {
items { userId displayName email }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [userId]
syncInterval: 3600
syncOn: [appForeground]

status:
showIndicator: true
position: header
showPendingCount: false

Sync Operations Screen​

The Sync component automatically registers a built-in screen at the route sync-operations that displays all sync instances, their cached models, and the operation queue.

Screen Sections​

  1. Status Bar: Current network state (Online/Offline), last sync timestamp, manual Sync Now button
  2. Sync Instances: Grouped by sync name, showing:
    • Each cached model with item count and freshness indicator
    • Pending queue items with operation type, timestamp, and retry count
  3. Queue Actions: Per-item Retry and Discard buttons
  4. History: Completed and failed operations with timestamps

Accessing the Screen​

- component: button
props:
label:
en-US: View Sync Status
onClick:
- navigate: "sync-operations"

The screen is also accessible via the sync status indicator when tapped.

Architecture​

Data Flow​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ YAML Sync Definition β”‚
β”‚ (models, queue, workflows, attachments) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ parsed at module load
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Sync Engine β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚Model Cache β”‚ β”‚ Operation Queue β”‚ β”‚
β”‚ β”‚(persisted) β”‚ β”‚ (Zustand+persist)β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Scheduler β”‚ β”‚Background Worker β”‚ β”‚
β”‚ β”‚(refetch) β”‚ β”‚(expo-task-mgr) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Sync Adapter Interface β”‚
β”‚ read() | queue() | upload() | status() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ consumed by
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ SDUI Components β”‚
β”‚ datasource | form | button | dataGrid β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Storage Strategy​

Data TypeStoragePersistence
Cached model dataSQLite (per-record rows) with an in-memory mirror for synchronous reads, namespaced per organizationSurvives app restart and re-authentication
Operation queueZustand store + AsyncStorageSurvives app restart
Attachment queueZustand store + file systemSurvives app restart
Sync configurationIn-memory (parsed from YAML)Re-parsed on module load
Network stateIn-memory (NetInfo)Real-time only

The model cache deliberately does not use the app's TanStack Query persister: that cache is domain-scoped and reset on authentication changes, while sync data must survive re-login and be partitioned per organization. Cached models are stored in SQLite as one row per record, so writes stay proportional to what changed, there is no practical size ceiling, and a future incremental (delta) sync only needs row upserts. The engine hydrates the in-memory mirror at startup and re-delivers restored models to open datasources; a previously persisted AsyncStorage cache is migrated automatically on first launch.

Queue Processing Order​

When connectivity is restored, the background worker processes operations in this order:

  1. Attachment uploads (files persisted locally, identified by their client-generated GUIDs)
  2. Workflows with dependsOn: [upload] (execute once the attachments referenced in their own inputs are uploaded)
  3. Independent workflows (no dependencies, processed in FIFO order)
  4. Model refresh (pull latest data after mutations complete)

Conflict Resolution​

The sync engine uses a last-write-wins strategy, with the server as the single source of truth:

  • Cached model data is a read-only replica. The client never merges entity state locally β€” all writes go through workflow executions.
  • Queued operations replay in their original order (subject to the processing order above) once connectivity is restored. Each replayed workflow runs full server-side validation and business logic, exactly as if it had been executed online.
  • If the same entity was modified on the server while the device was offline, the replayed workflow's changes overwrite those fields β€” the engine does not detect or surface concurrent-edit conflicts. This is the "last write" in last-write-wins: the queued operation is the later write.
  • If the server rejects a replayed workflow (entity deleted, invalid state transition, validation failure), the operation fails permanently per Failure Classification: onSyncFailed fires, rollbackAction reverts any optimistic patch, and the operation lands in the Sync Operations screen for the user to resolve.
  • After the queue drains, all models refresh (step 4 above). The refreshed server state replaces the local cache, including any remaining optimistic patches β€” the cache always converges on the server.

Choose queue mode plus clear "saved offline" messaging when concurrent edits or server-side rejections are likely; last-write-wins is safest for field-worker flows where each entity is effectively owned by one user at a time.

Best Practices​

  • Scope sync instances narrowly: Define per-module sync with only the data that module needs, rather than caching everything globally. This reduces storage usage and sync time.
  • Use dependsOn for attachment workflows: Always declare dependsOn: [upload] for workflows that reference uploaded file IDs. This ensures the server never receives an attachment reference before the file itself has arrived.
  • Set appropriate syncInterval: Frequently changing data (vehicles, orders) benefits from shorter intervals (300s). Reference data (locations, templates) can use longer intervals (3600s+).
  • Prefer queue mode for data-creation workflows: Use optimistic mode only when you can reliably predict the server result. For complex workflows with server-side validation, queue mode with clear "saved offline" messaging is safer.
  • Configure maxItems for large datasets: Prevent excessive storage usage by limiting cached items to what a field worker reasonably needs in a session.
  • Use syncOn: [manual] for large reference data: Inspection templates or configuration data that rarely changes should only sync on demand to avoid unnecessary network traffic.
  • Handle onSyncFailed gracefully: Provide clear user messaging and a path to the Sync Operations screen where they can retry or discard failed operations.
  • Test with airplane mode: Validate the complete offline flow by enabling airplane mode, performing all operations, then re-enabling connectivity to verify queue processing.