The two layers
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 aConsentConfigso 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.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:
SynheartWear.requestPermissions(...) for the PermissionType values you need (heartRate, hrv, sleep, steps, calories, distance).
Android — Health Connect + BLE. Add to AndroidManifest.xml:
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.plistentry required. RawCMMotionManageris permission-free on iOS —NSMotionUsageDescriptionis only needed forCMMotionActivityManager/CMPedometer, which Synheart does not use. - Android: no runtime permission required. Raw
SensorManagersensors are permission-free. (OnlyACTIVITY_RECOGNITIONwould be needed for the step-counter sensor, which Synheart does not use directly — step counts come from Health Connect underbiosignals.)
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:
Info.plist entry; iOS surfaces the prompt via UNUserNotificationCenter. Call/phone signals are no-op on iOS.
Android — add to AndroidManifest.xml:
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)
Handling denial and revocation
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
hasConsentto test for OS permission. It only reflects the Synheart layer. UserequestPermissions(Wear) or the platform’s own permission API. - Calling
grantConsentin production. It only mutates the local snapshot; without a JWT,hasConsentreturnsfalsewhenever a consent service is configured. UseconsentSubmitForm. - 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 theViewPermissionUsageActivityalias yourself, or Health Connect reads silently return empty. - Forgetting the notification listener service registration. Behavior consent +
BIND_NOTIFICATION_LISTENER_SERVICEare 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.
Related
- Consent System — full reference: types, tiers, channels, status, API.
- App policy — the per-app dashboard layer that backstops what your SDK is allowed to offer.
- Consent profiles — the per-app blueprints your SDK uses to render the consent UI.
- Capability System — the second authority alongside consent.
- Wear · Adapters —
PermissionType→ OS-scope mapping. - Behavior · Threat Model — the three-layer consent model for Behavior specifically.
- Troubleshooting — HealthKit / Info.plist gotchas.