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 |
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.| 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:
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
| 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. |
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.