Documentation Index
Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
Use this file to discover all available pages before exploring further.
Consent in a Synheart app lives on two independent layers. Both must be granted before the runtime ingests data; granting one does not imply the other. This page is the implementation guideline — read Consent System for the full reference and API surface.
The two layers
| Layer | Issued by | Authorises | Lives in |
|---|
| Synheart consent | Your app, via the SDK | Whether the runtime is allowed to process, fuse, and upload the data your app can read | ConsentSnapshot in secure storage, plus a cloud-issued JWT when the consent service is configured |
| Device permission | The operating system | Whether your app can read a sensor or data source at all | OS Settings → Privacy |
The two layers are independent. Synheart.hasConsent('biosignals') returns true as soon as the Synheart layer is granted, even if the user denied HealthKit. The data simply never arrives — adapters silently drop on the OS side. This is by design (consent state is portable across reinstalls; OS permission state is not), but it means you cannot use hasConsent to test for OS permission. Query SynheartWear.requestPermissions(...) for that.
Required order
Always request in this order. The reverse works mechanically but produces worse UX and higher decline rates.
- Initialise the SDK.
Synheart.initialize(...) with a ConsentConfig so the runtime knows whether a cloud consent service is configured.
- Synheart consent. Show your in-app consent UI, then either:
Synheart.grantConsent(...) — local-only (dev, tests, on-device tier).
Synheart.consentSubmitForm(...) — production; returns a JWT, stored automatically.
- OS permission(s). Request only the OS permissions corresponding to the consent types the user just granted (see mapping below).
- Start the module (
SynheartWear.startStreaming, SynheartBehavior.start, etc.). The runtime gates every sample on the Synheart layer; adapters gate every sample on the OS layer.
Synheart consent types
The runtime defines seven consent types. All default to denied.
| Type | What it gates | Wire string |
|---|
biosignals | HR, HRV, sleep, RR, wearable motion (Wear module) | biosignals |
phoneContext | Device motion, screen state, system state (Phone module) | phone_context |
behavior | Tap / scroll / swipe / typing timing (Behavior module) | behavior |
cloudUpload | HSI snapshot upload to Synheart Platform | cloud_upload |
syni | On-device LLM input (persona, conversation state) | syni |
vendorSync | RAMEN subscription to third-party vendor events | vendor_sync |
research | Lab-session JSON export | research |
Mapping: Synheart consent → device permissions
This is the canonical mapping, verified against the iOS, Android, and Flutter SDKs.
biosignals
iOS — HealthKit + BLE. Add to Info.plist:
<key>NSHealthShareUsageDescription</key>
<string>Synheart reads heart-rate, HRV, and sleep data to compute your HSI.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>Synheart writes derived metrics back to Health.</string>
<!-- Only if you use BLE HRM or the Garmin BLE companion -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Synheart connects to your BLE heart-rate monitor.</string>
Then call SynheartWear.requestPermissions(...) for the PermissionType values you need (heartRate, hrv, sleep, steps, calories, distance).
Android — Health Connect + BLE. Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
<uses-permission android:name="android.permission.health.READ_DISTANCE"/>
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_HISTORY"/>
<!-- WRITE_* equivalents only if you also write back -->
<!-- Android 12+ BLE HRM only -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<queries>
<package android:name="com.google.android.apps.healthdata"/>
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE"/>
</intent>
</queries>
You also need the Health Connect rationale intent-filter on your launcher activity and a ViewPermissionUsageActivity alias — see synheart-wear-flutter’s README for the full activity block.
Vendor cloud sources (WHOOP, Garmin Cloud, Fitbit): OAuth flow only — no OS permission.
phoneContext
The Phone module collects raw accelerometer/gyroscope via CMMotionManager (iOS) and SensorManager (Android), plus screen-state and system-state via OS callbacks.
- iOS: no
Info.plist entry required. Raw CMMotionManager is permission-free on iOS — NSMotionUsageDescription is only needed for CMMotionActivityManager / CMPedometer, which Synheart does not use.
- Android: no runtime permission required. Raw
SensorManager sensors are permission-free. (Only ACTIVITY_RECOGNITION would be needed for the step-counter sensor, which Synheart does not use directly — step counts come from Health Connect under biosignals.)
This is the one consent type where Synheart consent alone is enough.
behavior
Touch/scroll/typing timing is captured via view-tree hooks inside your app — no OS permission needed. Notification and call-state signals are separate and do require OS permission on each platform.
iOS:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
No Info.plist entry; iOS surfaces the prompt via UNUserNotificationCenter. Call/phone signals are no-op on iOS.
Android — add to AndroidManifest.xml:
<!-- Notification interaction tracking -->
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>
<!-- Call-state attention signals -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<service
android:name="ai.synheart.behavior.SynheartNotificationListenerService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService"/>
</intent-filter>
</service>
BIND_NOTIFICATION_LISTENER_SERVICE is a special permission — the user grants it in Settings → Notification access, not via a runtime dialog. READ_PHONE_STATE is a runtime permission. Tap / scroll / typing all work without either; the Behavior consent + view-tree hooks are sufficient. Notification and call signals just won’t be populated until the user grants those permissions.
cloudUpload
Network only — no OS permission. Make sure INTERNET and ACCESS_NETWORK_STATE are in your Android manifest (most apps have these already).
syni
On-device LLM — no OS permission. Microphone, if you wire voice input into Syni, is your app’s responsibility (NSMicrophoneUsageDescription on iOS, RECORD_AUDIO on Android).
vendorSync
OAuth credential for the vendor (issued by your backend, not an OS prompt). No device permission.
research
No additional OS permission beyond what the underlying data types already require. If your research export covers biosignals, you still need the HealthKit / Health Connect grants for biosignals.
Minimal implementation (Flutter)
// 1. Initialise.
await Synheart.initialize(
config: SynheartConfig(
appId: 'com.example.app',
subjectId: 'anon_user_123',
consentConfig: ConsentConfig(
appId: 'com.example.app',
appApiKey: appApiKey,
),
),
);
// 2. Synheart consent — show your in-app UI, then submit.
if (!await Synheart.hasConsent('biosignals')) {
await Synheart.consentSubmitForm(
deviceId: deviceId,
platform: Platform.isIOS ? 'ios' : 'android',
formJson: jsonEncode(consentForm),
);
}
// 3. OS permission — only after the user agreed in (2).
final os = await synheartWear.requestPermissions(
permissions: {
PermissionType.heartRate,
PermissionType.hrv,
PermissionType.sleep,
},
);
// 4. Start streaming. Adapters gate samples on the OS layer; the runtime
// gates fusion/upload on the Synheart layer.
if (os[PermissionType.heartRate] == ConsentStatus.granted) {
await synheartWear.startStreaming();
}
Swift and Kotlin shapes are identical — see Wear · Swift, Wear · Kotlin, Wear · Flutter.
Handling denial and revocation
| Scenario | What you see | What to do |
|---|
| User denies Synheart consent | hasConsent(type) == false; module never starts. | Show a “consent required” state. Don’t request OS permission — the prompt has no value yet. |
| User denies OS permission | requestPermissions returns denied; hasConsent is still true. | Show a rationale and re-request, or deep-link to OS settings. See Wear · Errors. |
| User revokes Synheart consent mid-session | Streams keep firing; fields derived from revoked data drop out; HSI confidence on affected axes falls. | Observe Synheart.onConsentChange and pause UI as needed. See What happens mid-session. |
| User revokes OS permission mid-session | Adapter stops receiving samples; runtime sees an empty stream. | Re-prompt the next time the feature is used. |
| Cloud JWT expires | getConsentStatus() returns expired. | Call ensureCloudConsentReady() — handled automatically on most paths. |
Local data is not deleted on either revocation. Call Synheart.wipeLocalData() if your policy requires a hard wipe.
Common mistakes
- Treating OS permission as consent. A user who tapped “Allow” on HealthKit has not consented to
cloudUpload. Each is separate.
- Using
hasConsent to test for OS permission. It only reflects the Synheart layer. Use requestPermissions (Wear) or the platform’s own permission API.
- Calling
grantConsent in production. It only mutates the local snapshot; without a JWT, hasConsent returns false whenever a consent service is configured. Use consentSubmitForm.
- Forgetting the Health Connect manifest plumbing on Android. The library manifest ships empty — you must declare every
health.READ_* permission, the rationale intent-filter, and the ViewPermissionUsageActivity alias yourself, or Health Connect reads silently return empty.
- Forgetting the notification listener service registration. Behavior consent +
BIND_NOTIFICATION_LISTENER_SERVICE are not enough on their own — you also need the <service> block in your manifest, and the user must enable it in Settings → Notification access.
- Adding
NSMotionUsageDescription “to be safe” on iOS. It’s harmless but misleading — Synheart doesn’t use the activity classifier, so the string never appears in any prompt.
- Requesting OS permission before Synheart consent. Mechanically valid; users decline at higher rates because the prompt arrives without context.
Account deletion
Synheart.requestAccountDeletion() overrides every consent type to denied for outbound traffic, regardless of stored state. OS permissions are untouched — that’s the user’s call. cancelAccountDeletion() lifts the override.