Skip to main content

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.

The Consent Module manages user permissions and enforces privacy boundaries across all Synheart Core modules. The runtime is the authoritative enforcer; this page describes what you control and how.

Core principles

  1. Explicit consent — every data-collection or upload operation requires an explicit grant.
  2. Granular control — module-level booleans plus per-channel flags.
  3. Revocable — consent can be withdrawn at any time; the runtime stops within the next cycle.
  4. Cloud-verified — when the consent service is configured, a valid JWT is required, not just a local boolean.
  5. Enforced — missing consent silently drops samples and uploads at the runtime seam; nothing fakes the data.

Synheart Core defines six consent types, mirrored across Dart, Kotlin, and Swift:
Consent typeWire stringModule / scopeWhat it enables
biosignals"biosignals"WearHR, HRV, sleep, motion from wearables, RR intervals.
phoneContext"phoneContext" / "phone_context"PhoneDevice motion, screen state, app context (no app names).
behavior"behavior"BehaviorTap/scroll/swipe/typing timing — no content.
cloudUpload"cloudUpload" / "cloud_upload"Cloud ConnectorHSI snapshot upload to the platform.
vendorSync"vendorSync" / "vendor_sync"RAMEN streamSubscription to third-party vendor events (WHOOP, Garmin webhooks).
research"research"Lab session exportLab-session JSON upload. Required for research-tier uploads.
All six default to denied. The user grants each one explicitly through a consent flow.

Granular channels

Each module can be consented at a finer grain via ConsentChannels:
GroupChannels
biosignalsvitals, sleep, cardio_advanced, neuromuscular, wearable_motion
phone_contextdevice_motion, device_context, system_state
behaviordigital_activity, notification_patterns, app_context
interpretationfocus_estimation, emotion_estimation
When a consent submission carries explicit channel flags, those win over the module-level boolean. When the channel map is absent or all-false but the module-level flag is true, the module-level boolean wins (sensible default for callers that don’t yet pass per-channel flags). ConsentTier describes the maximum processing destination:
TierWire valueAllowed
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.

The runtime computes a status from the local snapshot plus the cached cloud token:
StatusCondition
grantedCached JWT is present and not expired.
expiredCached JWT is present and past expiry.
pendingLocal grant exists but no token yet, or consent service is configured and the cycle is in flight.
deniedNo token and consent service is not configured.
Token refresh threshold is 5 minutesSynheart.consentNeedsTokenRefresh() returns true when the JWT expires within that window.
When the SDK initializes, the consent module loads any persisted snapshot. If nothing is stored, every type is denied.
await Synheart.initialize(
  config: SynheartConfig(
    appId: 'com.example.app',
    subjectId: 'anon_user_123',
    consentConfig: ConsentConfig(
      appId: 'com.example.app',
      appApiKey: appApiKey,
    ),
  ),
);

bool ready = await Synheart.hasConsent('biosignals');
if (!ready) {
  // Show your consent UI, then submit:
  final result = await Synheart.consentSubmitForm(
    deviceId: deviceId,
    platform: 'ios',
    formJson: jsonEncode(consentForm),
  );
  // result.token is the issued JWT; consent module stores it automatically.
}

2. Granting

Two paths grant consent:
  • Local grantSynheart.grantConsent(...) mutates the local snapshot. Useful for unit tests and dev. In production with a configured consent service, the runtime will still report pending until a JWT is issued.
  • Cloud submitSynheart.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.
What is not stored:
  • Private keys (those live in the Secure Enclave / Keystore via synheart-auth).
  • Raw form input fields beyond the consent submission.

4. Revocation

await Synheart.revokeConsentType('biosignals');   // single type
await Synheart.revokeConsent();                   // all types
Effects:
  • 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.

Enforcement

hasConsent semantics

hasConsent(type) =
  if cloudConsentConfigured AND consentStatus != granted:
    return false
  return localSnapshot.allows(type)
When the consent service is configured (typical in production), the local snapshot is not authoritative on its own — a valid cloud-issued JWT must be present.

Per-action gates

ActionRequired consent type(s)
Push HR / RR / vendor HRV / sleep stagesbiosignals
Push behavior eventsbehavior
Push phone contextphoneContext
HSI cloud uploadcloudUpload
RAMEN stream subscriptioncloudUpload AND vendorSync
Lab session exportresearch
The runtime applies these gates at the engine seam. Sample pushes that fail a gate are silently dropped.

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.
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.
This is automatic and not configurable from the SDK.

API reference

Querying state

// Snapshot summary (booleans + timestamp).
final snapshot = Synheart.shared.currentConsent;

// Coarse status enum.
final status = Synheart.getConsentStatus();
// Returns: ConsentStatus.granted | .pending | .denied | .expired

// Raw status map (status string + last-updated timestamp).
final raw = Synheart.consentStatus();

// True when the JWT expires within 5 minutes.
final needsRefresh = Synheart.consentNeedsTokenRefresh();

// Single-type query.
final ok = await Synheart.hasConsent('biosignals');

Mutating state

// Grant per channel (named-bool form is the canonical signature).
await Synheart.grantConsent(
  biosignals: true,
  behavior: true,
  phoneContext: false,
  cloudUpload: false,
);

await Synheart.revokeConsentType('biosignals');
await Synheart.revokeConsent();      // revoke every channel

Cloud submission

// Get the editable form (built from cached profile + current snapshot).
final form = await Synheart.consentGetEditableForm();

// Submit and receive a JWT. Signed by the device key automatically.
final result = await Synheart.consentSubmitForm(
  deviceId: deviceId,
  platform: 'ios',
  userId: userId,           // optional
  formJson: jsonEncode(updatedForm),
);

// Force a token refresh if needed.
await Synheart.ensureCloudConsentReady();
The consent service URL is read from ConsentConfig.consentServiceUrl (defaults to https://consent.synheart.ai) 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_id never leaves the device — only subject_hash.
  • Token material is in-memory only; never persisted in plaintext.
  • Revocation pauses outbound traffic on the next flush, not on a timer.