Skip to main content
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: 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.
Synheart Core defines seven consent types, mirrored across Dart, Kotlin, and Swift: 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: 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:
The runtime computes a status from the local snapshot plus the cached cloud token: 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.

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

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

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

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

Mutating state

Cloud submission

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.