Overview
Cross-device sync (SyniSync) lets one user’s devices share their personal baselines. A user who replaces a phone, or uses two, keeps “their normal” instead of warming up from zero on each device. Sync is device-first. Devices join a sync space: a private group of devices that share one encryption key. Every snapshot is encrypted on the device before upload, and the service stores and relays ciphertext between members of the space. No user account is required. Only baseline snapshots sync: the SRM state behind “your normal”. HSI windows, session summaries, raw wearable samples, and behavior events stay on the device that produced them.Sync runs in the native runtime. The Flutter, Kotlin, and Swift SDKs call the same runtime functions, so the behavior described on this page is the same on every platform.
How it works
1
The first device creates a space
The runtime generates a 256-bit Space Root Key (SRK) on the device and registers a new space. The create call returns a recovery key, which the user must keep.
2
It issues a pairing code
The first device asks for a short-lived, single-use pairing token, encrypts the SRK under a key derived from that token, and uploads the encrypted bundle.
3
The second device joins
The user enters the token on the second device. That device downloads the bundle, derives the same key from the token, decrypts the SRK, and becomes a member of the space.
4
Devices push and pull
Each sync cycle pushes new baseline snapshots and pulls the ones the other members pushed. The next cycle on a newly joined device pulls every baseline the other devices have pushed.
Encryption
- Each baseline snapshot is encrypted with XChaCha20-Poly1305 under its own key, derived from the SRK with HKDF-SHA256 and salted with the snapshot’s ID.
- The envelope header (artifact ID, type, subject, session, time range, sequence number, schema) is bound into the cipher as associated data, so it cannot be swapped onto a different payload.
- On pull, the runtime checks a SHA-256 of the decrypted payload and rejects any snapshot whose sequence number goes backwards.
- At rest, the SRK is stored wrapped. On mobile the wrapping key lives in the platform secure store (Keychain or Android Keystore); on desktop it is derived from the HMAC signing secret.
Conflicts
A snapshot is never overwritten once a device has it: the runtime skips an incoming snapshot whose ID is already stored, and the sequence-number check drops replayed or older versions. The runtime reports those rejections asconflicts_resolved in the sync result (conflictsResolved in Swift).
Prerequisites
Every sync operation needs all of the following:- Sync enabled in config with a platform origin (see Enable sync).
- The sync feature activated:
SynheartFeature.synsyncin Flutter,.synsyncin Swift,SynheartFeature.CLOUDin Kotlin. cloudUploadconsent granted by the user. Sync shares the consent and capability tier of cloud upload.- A registered device. On mobile, sync requests are signed with the device key from device registration. Desktop hosts can sign with HMAC credentials instead (see Runtime configuration).
checkSyncReadiness includes it for you.
Sync is not available on the Wear OS or watchOS device role.
Enable sync
- Flutter
- Kotlin
- Swift
baseUrl is the platform origin, not a sync-specific path. In Swift, an empty baseUrl falls back to cloudConfig.baseUrl. Self-hosted deployments can point sync at its own host with enterprise.sync_url in synheart.json.
setSyncEnabled(false) turns sync off at runtime without leaving the space. Turning it back on restores the same membership.
Check readiness
CallcheckSyncReadiness before showing a sync action. It combines the SDK gates (activation, consent, capability) with the native snapshot and tells you the first prerequisite that is missing for that specific operation.
Create, join, and recover do not need an existing space. Only
generatePairing and syncNow need the space key. clearLocalSpace stays available even after consent is withdrawn or the device is revoked, so users always have a way to start over.
Kotlin uses the same names in upper snake case (SyncOperation.GENERATE_PAIRING, SyncReadinessCode.ACTIVE_SPACE_REQUIRED).
Create a space
Pair another device
On the device that is already in the space:expires_in. Tokens are single-use and short-lived, and the service sets the lifetime, so rely on expires_in rather than a fixed duration. If a token expires, generate a new one.
On the new device:
Run a sync cycle
syncNow() when it suits your app, for example when the app comes to the foreground or after a session ends, since that is when baselines update. A cycle pushes in batches of 50 and pulls in pages of 100 until the space has nothing new.
syncNow() does network I/O. Flutter and Swift run it off the UI thread for you. In Kotlin every sync call is blocking, so call it from a background dispatcher:
Manage devices
syncListDevices() for a “Synced with N devices” line. Each entry has device_id, device_name, is_primary, trusted_at, last_seen_at, and revoked.
Revoking a device removes it from the space on the server, so it can no longer push or pull. It does not rotate the space key, and data the device already pulled stays on that device.
Leave, delete, or start over
Confirm with the user before
syncDeleteSpace(). It cannot be undone, and the recovery key stops working.
syncClearLocalSpace() is the local escape hatch: it works without network and without consent, for a device that is stuck in a space it can no longer reach. To remove the device from the server as well, revoke it from another member.
Recover a space
When every device is lost, a new device can rejoin with the recovery key and the space ID:srkUnavailable: the device is still a member but has lost its local copy of the key, for example after the app’s secure storage was cleared.
Errors
In Flutter and Kotlin, a failed sync call throwsSyncNativeException. In Swift, the space and device calls do not throw: a failed call returns the error fields in the result dictionary, with the message under "error". All three expose the same fields: code, message (safe to show), retryable, retryAfterMs, owner, and recovery.
Do not re-register the device in response to a membership error. Membership codes mean the device is registered but no longer in the space; re-read readiness and offer to join or create a space instead.
Platform notes
Method names are the same in all three SDKs:
syncCreateSpace, syncGeneratePairing, syncJoinSpace, syncRecoverSpace, syncListDevices, syncRevokeDevice, syncLeaveSpace, syncDeleteSpace, syncClearLocalSpace, syncNow, setSyncEnabled, and checkSyncReadiness. Hosts that call the runtime directly can use the sync FFI functions.
Without the cloud
If a user cannot or will not use sync,baselineExportOffline(passphrase:) and baselineImportOffline move baselines between devices as a passphrase-encrypted file instead. Nothing passes through the service.