Skip to main content
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

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.
  1. Initialise the SDK. Synheart.initialize(...) with a ConsentConfig so the runtime knows whether a cloud consent service is configured.
  2. 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.
  3. OS permission(s). Request only the OS permissions corresponding to the consent types the user just granted (see mapping below).
  4. 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.
The runtime defines seven consent types. All default to denied. This is the canonical mapping, verified against the iOS, Android, and Flutter SDKs.

biosignals

iOS — HealthKit + BLE. Add to Info.plist:
Then call SynheartWear.requestPermissions(...) for the PermissionType values you need (heartRate, hrv, sleep, steps, calories, distance). Android — Health Connect + BLE. Add to AndroidManifest.xml:
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:
No 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)

Swift and Kotlin shapes are identical — see Wear · Swift, Wear · Kotlin, Wear · 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 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.