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

# Consent System

> User permission management — types, tiers, granular channels, and how the runtime enforces them

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.

***

## 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)                                                                                                      |

The layers compose by AND: an app cannot enable what the platform has disabled, and a user cannot exercise what the app's policy forbids. The consent service enforces the platform ↔ app layer at write time (`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.        |

All seven 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`:

| 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`                                 |

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

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

Token refresh threshold is **5 minutes** — `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 is `denied`.

```dart theme={null}
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 grant** — `Synheart.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 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`.

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

```dart theme={null}
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.

### 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` / `onStateUpdate` streams 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.onConsentChange` if you need to react (pause UI, surface a "consent required" banner, etc.).

Re-granting consent resumes ingest on the next cycle without restarting the SDK; buffered cloud uploads from before the revocation are not replayed unless explicitly re-queued.

***

## Enforcement

### `hasConsent` semantics

```text theme={null}
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

| 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`                                                                       |

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.

***

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

This is automatic and not configurable from the SDK.

***

## API reference

### Querying state

```dart theme={null}
// 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

```dart theme={null}
// 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

```dart theme={null}
// 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://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_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.

***

## Related

* [Cloud Protocol](/synheart-core/cloud-protocol) — what `cloudUpload` actually authorises.
* [Capability System](/synheart-core/capability-system) — the second authority alongside consent.
* [Synheart Auth — Signing](/synheart-auth/signing) — how consent submissions are device-signed.
* [Synheart Behavior — Threat Model](/synheart-behavior/threat-model) — what `behavior` consent unlocks at the SDK layer.
