Core principles
- Explicit consent — every data-collection or upload operation requires an explicit grant.
- Granular control — module-level booleans plus per-channel flags.
- Revocable — consent can be withdrawn at any time; the runtime stops within the next cycle.
- Cloud-verified — when the consent service is configured, a valid JWT is required, not just a local boolean.
- Enforced — missing consent silently drops samples and uploads at the runtime seam; nothing fakes the data.
Three-layer authority
Before a sensitive feature fires — Syni chat, lab export, vendor sync, HSI cloud upload — three independent gates must all be open:| Layer | Controlled by | Stored as | Granularity |
|---|---|---|---|
| Platform capability | Synheart (master switch) | platform_capabilities.feature_key | Product-wide (syni_integration, research_export, vendor_sync, cloud_processing, hsi_uploads, wear_integration, lab_ingest) |
| App policy | App owner via the dev portal | app_policies.allow_* columns | Per app (allow_syni, allow_research, allow_cloud_processing, allow_hsi_uploads, vendor_sync_allowed) |
| User consent | End user via the consent UI | ConsentSnapshot (this page) | Per channel (the seven types below) |
PUT /v1/apps/{app_id}/policy rejects bits the platform mask doesn’t grant — e.g. allow_syni=true is refused unless syni_integration is enabled); the runtime enforces the app ↔ user layer when issuing the JWT and at the per-action gates listed below.
This page covers the user-consent layer. The other two are out of band for the SDK consumer: platform capability is opaque to the app, and app policy is read-only from the device — it shapes which consent types the user is allowed to grant in the first place.
Consent types
Synheart Core defines seven consent types, mirrored across Dart, Kotlin, and Swift:| Consent type | Wire string | Module / scope | What it enables |
|---|---|---|---|
biosignals | "biosignals" | Wear | HR, HRV, sleep, motion from wearables, RR intervals. |
phoneContext | "phoneContext" / "phone_context" | Phone | Device motion, screen state, app context (no app names). |
behavior | "behavior" | Behavior | Tap/scroll/swipe/typing timing — no content. |
cloudUpload | "cloudUpload" / "cloud_upload" | Cloud Connector | HSI snapshot upload to the platform. |
syni | "syni" | Syni | On-device LLM input (persona, conversation state). |
vendorSync | "vendorSync" / "vendor_sync" | RAMEN stream | Subscription to third-party vendor events (WHOOP, Garmin webhooks). |
research | "research" | Lab session export | Lab-session JSON upload. Required for research-tier uploads. |
Granular channels
Each module can be consented at a finer grain viaConsentChannels:
| Group | Channels |
|---|---|
biosignals | vitals, sleep, cardio_advanced, neuromuscular, wearable_motion |
phone_context | device_motion, device_context, system_state |
behavior | digital_activity, notification_patterns, app_context |
interpretation | focus_estimation, emotion_estimation |
true, the module-level boolean wins (sensible default for callers that don’t yet pass per-channel flags).
Consent tiers
ConsentTier describes the maximum processing destination:
| Tier | Wire value | Allowed |
|---|---|---|
local | "local" | On-device only — nothing leaves the device. Default. |
cloud | "cloud" | Derived/aggregated data may upload to Synheart Platform. |
research | "research" | Raw data may be exported to a research lab. Implies cloud. |
Consent status
The runtime computes a status from the local snapshot plus the cached cloud token:| Status | Condition |
|---|---|
granted | Cached JWT is present and not expired. |
expired | Cached JWT is present and past expiry. |
pending | Local grant exists but no token yet, or consent service is configured and the cycle is in flight. |
denied | No token and consent service is not configured. |
Synheart.consentNeedsTokenRefresh() returns true when the JWT expires within that window.
Consent flow
1. Initial consent request
When the SDK initializes, the consent module loads any persisted snapshot. If nothing is stored, every type isdenied.
2. Granting
Two paths grant consent:- Local grant —
Synheart.grantConsent(...)mutates the local snapshot. Useful for unit tests and dev. In production with a configured consent service, the runtime will still reportpendinguntil a JWT is issued. - Cloud submit —
Synheart.consentSubmitForm(...)sends a signed form to the consent service and stores the returned JWT. This is the production path.
3. Storage
The consent module persists state via the platform’s secure storage (Keychain on iOS, EncryptedSharedPreferences on Android,keyring on desktop). The runtime registers its own secure-storage callbacks during init; you don’t manage this directly.
What is stored:
- The local
ConsentSnapshot(per-type booleans, channels, tier, timestamps). - The cached
ConsentToken(JWT, profile id, expiry, scopes). - Per-platform storage keys are scoped per
subject_id.
- Private keys (those live in the Secure Enclave / Keystore via
synheart-auth). - Raw form input fields beyond the consent submission.
4. Revocation
- Each module checks consent before pushing samples; revoked modules’ samples are silently dropped at the runtime engine seam.
- Cloud uploads stop on the next flush — the runtime’s connector hook reads the consent state before draining the queue.
- Local data is not deleted automatically. Use
Synheart.wipeLocalData()if you need a hard wipe.
What happens mid-session
Revocation is non-destructive to your app: nothing throws, no streams close.- Sample ingest stops at the seam. Modules continue running, but samples for revoked types are dropped before they reach inference. Your
onHSIUpdate/onStateUpdatestreams keep firing — they just stop containing fields derived from revoked data, and confidence on affected axes drops. - In-flight cloud uploads continue; queued ones don’t. Whatever is already on the wire when the user revokes will complete. Anything still in the local queue is held until consent is re-granted, or discarded on
wipeLocalData(). - Already-emitted HSI events are not retroactively recalled. If your app has buffered HSI envelopes from before the revocation, that data is yours to delete or retain per your own policy.
- No app-level callback fires. Observe consent state via
Synheart.onConsentChangeif you need to react (pause UI, surface a “consent required” banner, etc.).
Enforcement
hasConsent semantics
Per-action gates
| Action | Required consent type(s) |
|---|---|
| Push HR / RR / vendor HRV / sleep stages | biosignals |
| Push behavior events | behavior |
| Push phone context | phoneContext |
| HSI cloud upload | cloudUpload |
| RAMEN stream subscription | cloudUpload AND vendorSync |
| Syni chat / cloud relay | syni (plus app policy allow_syni and platform capability syni_integration) |
| Lab session export | research |
Account-deletion override
Synheart.requestAccountDeletion() puts the runtime in a wind-down state that overrides every consent type to denied for outbound traffic, regardless of stored state. cancelAccountDeletion() lifts the override.
Cold-start consent buffer
The runtime opens an HSI session immediately, but the consent token may take seconds to fetch on a fresh app launch. To avoid losing the first window:- The auto-enqueue bridge buffers up to 8 pending HSI windows when consent is
pending. - On
granted, the buffer drains and replays into the upload queue. - If consent never lands, the oldest buffered windows drop FIFO.
API reference
Querying state
Mutating state
Cloud submission
ConsentConfig.consentServiceUrl (defaults to https://api.synheart.ai/v1/consent) or your enterprise override.
Privacy invariants
These hold regardless of consent state:- The SDK never collects content (no text, no clipboard payloads, no notification bodies, no audio).
- Raw
subject_idnever leaves the device — onlysubject_hash. - Token material is in-memory only; never persisted in plaintext.
- Revocation pauses outbound traffic on the next flush, not on a timer.
Related
- Cloud Protocol — what
cloudUploadactually authorises. - Capability System — the second authority alongside consent.
- Synheart Auth — Signing — how consent submissions are device-signed.
- Synheart Behavior — Threat Model — what
behaviorconsent unlocks at the SDK layer.