> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getbased.health/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage schema

> Local-first storage keys, importedData shape, IndexedDB use, and sync-relevant state.

# Storage Schema

getbased stores normal app data in the browser. `localStorage` holds app data and preferences, and IndexedDB holds auto-backup snapshots plus larger per-device datasets. Opt-in features may use hosted encrypted storage outside this browser; profile sharing is documented separately below because the hosted record contains only ciphertext and share metadata.

## localStorage key reference

Keys are namespaced by profile ID where data is per-profile. `{profileId}` defaults to `"default"` for the first profile.

### Global keys (not profile-specific)

| Key | Type | Purpose |
| - | - | - |
| `labcharts-profiles` | JSON array or passphrase ciphertext | Profile index: `[{ id, name, createdAt }]`; sensitive when passphrase protection is enabled |
| `labcharts-ai-provider` | string | Active AI provider: `'openrouter'` \| `'routstr'` \| `'ppq'` \| `'venice'` \| `'custom'` \| `'ollama'` \| `'codex-agent'` (CLI source; the UI tab is `cli`) |
| `labcharts-chat-backend` | string | Direct versus CLI chat backend; use `agent-chat-settings.ts` accessors rather than inferring from a saved API key |
| `labcharts-agent-host-*` | device-scoped configuration/protected credential | Companion connection and agent/target model/effort selections; not portable CLI authentication |
| `labcharts-agent-model-catalog-*` | cached metadata | Capability catalog owned by the exact agent and execution target; invalidate on target changes |
| `labcharts-chat-panel-width`, `labcharts-chat-rail-width` | number string | Device-local desktop dimensions, clamped when applied |
| `labcharts-chat-reasoning-effort-v1:{provider}:{model}` | string | Direct/local-model effort preference, with encoded provider/model components; CLI preferences use agent/target accessors instead |
| `labcharts-openrouter-key` | protected string | OpenRouter API key; `d1:` device envelope without a passphrase or passphrase ciphertext when enabled |
| `labcharts-routstr-key` | protected string | Routstr node session key; same credential-wrapping rule |
| `labcharts-routstr-node` | string | Selected Routstr node URL (from Nostr discovery) |
| `labcharts-cashu-wallet-mint` | string | Cashu wallet mint URL (mirror of IDB meta for sync) |
| `labcharts-cashu-wallet-mnemonic` | string | BIP-39 wallet seed phrase (encrypted via `encryptedSetItem`) |
| `labcharts-ppq-key` | protected string | PPQ API key |
| `labcharts-venice-key` | protected string | Venice AI API key |
| `labcharts-custom-key` | protected string | Custom API key |
| `labcharts-lens-key` | protected string | External Knowledge Base key |
| `labcharts-ollama` | protected JSON | Local AI config: `{ url, model, mode, apiKey }`. Key kept as `ollama` for backwards compatibility |
| `labcharts-xai-voice-key` | protected string | xAI voice key |
| `labcharts-elevenlabs-voice-key` | protected string | ElevenLabs voice key |
| `labcharts-voice-local-server-key` | protected string | Device-scoped compatible voice-server key |
| `labcharts-meteo-config` | protected JSON | Sun data-source URL/bearer configuration |
| `labcharts-ollama-pii-model` | string | Local AI model used for PII obfuscation (can differ from main chat model) |
| `labcharts-nutrition-ai-route` | JSON | Optional `{ provider, model }` route for meal photos and nutrition labels; separate from main chat selection |
| `nutrition-history-range` | string | Device UI preference: `30d` \| `3m` \| `6m` \| `1y` \| `all` |
| `nutrition-history-view` | string | Device UI preference: `meals` \| `trends` |
| `labcharts-openrouter-model` | string | Selected OpenRouter model ID (e.g., `anthropic/claude-sonnet-4-6`) |
| `labcharts-routstr-model` | string | Selected Routstr model ID |
| `labcharts-ppq-model` | string | Selected PPQ model ID |
| `labcharts-venice-model` | string | Selected Venice model ID |
| `labcharts-openrouter-models` | JSON | Cached model list from OpenRouter |
| `labcharts-openrouter-pricing` | JSON | Cached per-token pricing from OpenRouter |
| `labcharts-routstr-models` | JSON | Cached model list from Routstr |
| `labcharts-ppq-models` | JSON | Cached model list from PPQ |
| `labcharts-venice-models` | JSON | Cached model list from Venice |
| `labcharts-marker-desc` | JSON | Cached custom marker descriptions from AI |
| `labcharts-time-format` | string | `'24h'` \| `'12h'` — time display preference |
| `labcharts-debug` | string | Debug mode flag — enables console output and diff viewer |
| `labcharts-pii-choice` | sessionStorage | One-time PII warning flag (sessionStorage, not localStorage) |
| `labcharts-encryption-enabled` | string | `'true'` when passphrase protection is enabled |
| `labcharts-encryption-salt` | string | Global Base64 PBKDF2 salt for the passphrase-derived key |
| `labcharts-cloud-ai-consent` | JSON | Versioned, recipient-scoped cloud AI/voice approvals on this browser |
| `labcharts-hosted-wearable-consent` | JSON | Versioned profile/provider-scoped operated wearable-relay approvals |
| `labcharts-sync-enabled` | string | `'true'` means this browser has a configured Sync identity; it does not by itself mean relay traffic is active |
| `labcharts-sync-paused` | string | `'true'` pauses a configured identity on this browser without deleting its Evolu owner or local Sync history |
| `labcharts-sync-backup-restore-pending` | JSON array | Durable profile-ID handoff that makes restored profiles publish before the first inbound merge |
| `getbased-profile-shares-v1` | JSON array | Active profile share links created on this browser: share id, profile id/name, share URL, management token, createdAt, expiresAt. Does not include the share password or plaintext profile data |

### Per-profile keys

| Key | Type | Purpose |
| - | - | - |
| `labcharts-{profileId}-imported` | JSON (may be encrypted) | Main data store — see importedData structure below |
| `labcharts-{profileId}-units` | string | `'EU'` (International/SI) \| `'ANZ'` (Australia/New Zealand) \| `'US'` display-unit profile |
| `labcharts-{profileId}-rangeMode` | string | `'optimal'` \| `'reference'` \| `'both'` chart range mode |
| `labcharts-{profileId}-noteOverlay` | string | `'off'` \| `'on'` — note dots on charts |
| `labcharts-{profileId}-suppOverlay` | string | `'off'` \| `'on'` — supplement bars on charts |
| `labcharts-{profileId}-phaseOverlay` | string | `'off'` \| `'on'` — cycle phase pills at observed chart points |
| `labcharts-{profileId}-chatPersonality` | string | Active personality ID: `'default'`, `'house'`, or a stable `custom_...` ID |
| `labcharts-{profileId}-chatPersonalityCustom` | JSON array (may be encrypted) | Custom personas with stable ID, name/icon, editable prompt, timestamps, and optional hosted-use agreement |
| `labcharts-{profileId}-chatPersonalityDeleted` | JSON map (may be encrypted) | Persona deletion tombstones: `{ [customPersonaId]: deletedAtMs }` |
| `labcharts-{profileId}-chatRailOpen` | string | `'true'` \| `'false'` — chat thread rail visibility |
| `labcharts-{profileId}-chat` | JSON (may be encrypted) | Legacy chat history (migrated to threads on first load) |
| `labcharts-{profileId}-chat-threads` | JSON array (may be encrypted) | Thread index with timestamps/count, persona, discussion state, optional fork-source metadata, bounded `projectName`, and `pinned`; projects use this index, not a separate plaintext store |
| `labcharts-{profileId}-chat-t_{id}` | JSON array (may be encrypted) | Per-thread normalized user/assistant messages and persisted metadata |
| `labcharts-{profileId}-chatDraft_{id}` | string (may be encrypted) | Device-local unsent composer text for one thread; excluded from Sync and backups |
| `labcharts-{profileId}-contextHealth` | JSON | Cached AI health dots: `{ dots, summaries, fingerprints }` |
| `labcharts-{profileId}-focusCard` | JSON | Cached AI focus card: `{ fingerprint, text }` |
| `labcharts-{profileId}-tour` | string | `'completed'` — app tour completion flag |
| `labcharts-{profileId}-cycleTour` | string | `'completed'` — cycle tour completion flag |
| `labcharts-{profileId}-onboarded` | string | `'profile-set'` \| `'dismissed'` — onboarding state |
| `labcharts-{profileId}-sync-dirty` | generation token | Durable local-edit marker. Pulls flush this generation before applying remote state |
| `labcharts-{profileId}-delta-{surface}` | JSON map | Last successfully pushed item hashes for one delta surface; related `-meta` keys prevent stale completion races |
| `labcharts-{profileId}-sync-cutover-v2` | string | Delta-cutover gate. Backup restore clears it so the first recovery push also includes a complete compatibility payload |
| `labcharts-{profileId}-sync-ts` | string | Last profile Sync timestamp. Backup restore clears it before recovery |
| `labcharts-{profileId}-sex` | string | `'male'` \| `'female'` — profile sex |
| `labcharts-{profileId}-dob` | string | `'YYYY-MM-DD'` — date of birth |
| `labcharts-{profileId}-country` | string | ISO country code |
| `labcharts-{profileId}-zip` | string | ZIP/postal code |

## importedData structure

Stored as JSON at `labcharts-{profileId}-imported`. Full database and folder backups preserve this blob, while curated single-profile exports select user-facing fields from it.

```js theme={null}
{
  // Live lab results — one active value per marker/date. Per-file history is
  // stored separately in importSnapshots[].
  entries: [
    {
      date: "2025-03-15",            // ISO date string
      context: {
        sampleTime: "07:35",          // specimen collection/draw time
        fasting: true,                // true | false; absent means unknown
        cycleDay: 21,
        cyclePhase: "luteal",
        cyclePhaseDetail: "mid_luteal",
        cyclePhaseSource: "recorded"
      },
      collectionContextSources: {
        sampleTime: "snap_1782290000000_abcd",
        fasting: "snap_1782290000000_abcd"
      },
      markers: {
        "biochemistry.glucose": 5.2, // category.markerKey → numeric value
        "hormones.testosterone": 18.4,
        // ... all measured markers for this date
      },
      markerSources: {
        "biochemistry.glucose": "snap_1782290000000_abcd"
      }
    }
    // ... more entries
  ],

  // Per-file AI import history. entries[] holds the current live marker values;
  // importSnapshots[] preserves which source file created/reviewed them.
  importSnapshots: [
    {
      id: "snap_1782290000000_abcd",
      date: "2025-03-15",
      fileName: "lab-report.pdf",
      testType: "blood",
      importedAt: 1782290000000,
      importHash: "sha256-or-parser-hash",
      sampleTime: "07:35",
      fasting: true,
      collectionContextApplied: ["sampleTime", "fasting"],
      adoptReferenceRanges: true,
      costInfo: { provider: "openrouter", modelId: "..." },
      markers: [
        {
          rawName: "Glucose",
          mappedKey: "biochemistry.glucose",
          value: 5.2,
          unit: "mmol/L",
          refMin: 3.9,
          refMax: 5.6,
          matched: true
        }
      ],
      excludedIndices: []
    }
  ],

  // Device-local diagnostics for AI lab-import attempts. These are capped,
  // excluded from cross-device sync, and preserved during inbound sync merges.
  importBenchmarks: [
    {
      id: "bench_1782290000000_abcd",
      status: "confirmed", // or reference-scored/reference-passed/diagnostic status
      benchmarkAt: 1782290000000,
      fileName: "lab-report.pdf",
      inputHash: "...",    // same-input comparison identity for personal reports
      provider: "ollama",
      modelId: "qwen3:14b",
      importMode: "text",
      timings: { analysisMs: 9200, piiMs: 450 },
      usage: { inputTokens: 3100, outputTokens: 1400 },
      runtime: {
        provider: "ollama",
        executionLocation: "local",
        contextLength: 16384,
        quantLevel: "Q4_K_M",
        loadedAtStart: true
      }
    }
  ],
  deletedImportBenchmarkIds: [],

  // entries[].markerSources and collectionContextSources link current live
  // fields back to snapshots. Snapshot delete/re-review must mutate through
  // data-merge/lab-entry helpers so owners, fallbacks, and tombstones remain
  // consistent across same-date reports and Sync.

  // Standalone notes (independent of lab dates)
  notes: [
    { date: "2025-03-10", text: "Started vitamin D protocol" }
  ],

  // Supplements and medications: compatibility-first v2 therapy records
  supplements: [
    {
      id: "sm_1786000000000_abcd",   // stable delta/sync identity
      schemaVersion: 2,
      name: "Magnesium complex",
      type: "supplement",            // or "medication"
      brand: "Example brand",
      genericName: "Magnesium",
      dosageForm: "capsule",
      route: "oral",

      // Printed product facts, separate from the user's regimen
      servingSize: { value: 2, unit: "capsule" },
      labelDirections: "Take two capsules daily",
      ingredients: [
        {
          name: "Magnesium bisglycinate",
          amount: "200 mg",
          amountValue: 200,
          amountUnit: "mg",
          basis: "per serving",
          timesPerDay: 2              // optional row override
        }
      ],
      inactiveIngredients: ["Hypromellose capsule", "Rice flour"],

      // Personal regimen and lifecycle
      dosage: "With food before bed",
      timesPerDay: 1,
      schedule: { mode: "daily", timesPerDay: 1 },
      periods: [
        { start: "2025-01-01", end: "2025-03-31", dose: "1 capsule" },
        { start: "2025-05-01", end: null, dose: "2 capsules" }
      ],
      startDate: "2025-01-01",
      endDate: null,                  // legacy compatibility mirrors
      currentDose: "2 capsules",
      lifecycle: { state: "active", changedAt: 1786000000000 },

      // Source-reported laboratory evidence, never folded into ingredients
      qualityEvidenceScope: "matching-lot",
      qualityTests: [
        {
          category: "contaminant",
          analyte: "Lead",
          canonicalAnalyte: "lead",
          resultText: "< 0.5 mcg",
          comparator: "<",
          value: 0.5,
          unit: "mcg",
          basis: "per serving",
          status: "reported",
          includeInAIContext: true,
          sourceKinds: ["product URL"]
        }
      ],
      reason: "Low magnesium intake",
      updatedAt: 1786000000000
    }
  ],

  // Context cards — all nullable (null = not filled by user)
  diagnoses: {                          // Medical History card
    conditions: [
      { name: "Hashimoto's", severity: "major", since: "2020" }
    ],
    familyHistory: [                    // first-degree + grandparents
      // relative ∈ {mother, father, sibling, child,
      //             maternal_grandmother, maternal_grandfather,
      //             paternal_grandmother, paternal_grandfather}
      { relative: "father", condition: "Heart Attack (MI)", onsetAge: 52, note: "survived" },
      { relative: "mother", condition: "Type 2 Diabetes", onsetAge: 45 }
    ],
    note: ""
  },

  diet: {                               // Diet card
    type: "omnivore",
    restrictions: ["gluten-free", "seed-oil-free"],
    pattern: "intermittent_fasting",
    breakfast: "eggs and avocado",
    breakfastTime: "09:00",             // 24h format
    lunch: "salad with protein",
    lunchTime: "13:00",
    dinner: "meat and vegetables",
    dinnerTime: "18:00",
    snacks: "",
    snacksTime: "",
    note: ""
  },

  exercise: {                           // Exercise card
    frequency: "4-5x_week",
    types: ["strength", "walking"],
    intensity: "moderate",
    dailyMovement: "active",
    note: ""
  },

  sleepRest: {                          // Sleep & Rest card
    duration: "7-8h",
    quality: "good",
    schedule: "consistent",
    roomTemp: "cool",                   // circadian-informed
    issues: ["occasional_waking"],
    environment: ["blackout_curtains", "emf_off"],
    practices: ["mouth_taping", "magnesium"],
    note: ""
  },

  lightCircadian: {                     // Light & Circadian card
    amLight: "sunrise_sunlight",        // circadian-informed
    daytime: "outdoor_work",
    uvExposure: "daily",
    evening: ["blue_light_glasses", "dim_lights"],
    screenTime: "low",
    techEnv: ["wifi_off_night"],
    cold: "cold_shower",
    grounding: "daily_earthing",
    mealTiming: ["time_restricted"],
    note: ""
  },

  stress: {                             // Stress card
    level: "moderate",
    sources: ["work", "finances"],
    management: ["meditation", "exercise"],
    note: ""
  },

  loveLife: {                           // Love Life & Relationships card
    status: "partnered",
    relationship: "good",
    satisfaction: "satisfied",
    libido: "normal",
    frequency: "weekly",
    orgasm: "yes",
    concerns: [],
    note: ""
  },

  environment: {                        // Environment card
    setting: "suburban",
    climate: "temperate",
    water: "reverse_osmosis",          // circadian-informed
    waterConcerns: [],
    emf: ["wifi", "smart_meter"],
    emfMitigation: ["router_timer"],
    homeLight: "led_warm",
    air: ["hepa_filter"],
    toxins: [],
    building: "modern",
    note: ""
  },

  // Full-width card — freetext string
  interpretiveLens: "Longevity medicine, quantum biology, functional endocrinology",

  // Health goals — priority-ordered array
  healthGoals: [
    { text: "Optimize testosterone naturally", severity: "major" },
    { text: "Improve sleep quality",           severity: "mild"  },
    { text: "Reduce inflammation markers",     severity: "minor" }
  ],

  // Free-form AI context notes — freetext string
  contextNotes: "Currently experimenting with carnivore diet.",

  // Menstrual cycle — null for male profiles
  menstrualCycle: {
    cycleLength: 28,           // days (auto-calculated if enough periods)
    periodLength: 5,           // days
    regularity: "regular",     // 'regular' | 'irregular' | 'very_irregular'
    flow: "moderate",
    contraceptive: "none",
    conditions: "none",
    periods: [
      {
        startDate: "2025-02-01",
        endDate:   "2025-02-06",
        flow: "moderate",
        symptoms: ["cramps", "fatigue"],
        notes: ""
      }
    ]
  },

  // Per-marker reference range overrides
  refOverrides: {
    "biochemistry.glucose": {
      refMin: 4.0,
      refMax: 5.5,
      optimalMin: 4.0,
      optimalMax: 5.0,
      labRefMin: 3.9,   // stashed lab-stated range from PDF import (for two-step revert)
      labRefMax: 5.6,   // preserved when user manually edits — revert goes lab → schema default
      labRefDate: "2025-03-15",
      labRefSnapshotId: "snap_1782290000000_abcd",
      refSource: "manual" // "import" | "manual" — tracks who set the current refMin/refMax
    }
    // ... user-customized ranges from detail modal editing or import-time range adoption
  },

  // Display overrides for category labels (from rename)
  categoryLabels: {
    "mylab": "My Laboratory"  // categoryKey → display name
  },

  // Display overrides for category icons (from emoji picker)
  categoryIcons: {
    "mylab": "🧪"             // categoryKey → emoji
  },

  // Optional visual category overrides keyed by immutable marker identity.
  // Values and companion metadata remain at their original category.markerKey.
  markerPlacements: {
    "gb:marker:glucose": { categoryKey: "energyMetabolism" },
    "custom:opaque_id": { categoryKey: "biochemistry" }
  },

  // Display-name overrides. Most keys are marker dotKeys; import-scoped
  // metadata can use "category.markerKey:YYYY-MM-DD".
  markerLabels: {
    "biochemistry.glucose": "Fasting glucose"
  },

  // Custom markers from PDF import — keyed by "category.markerKey"
  customMarkers: {
    "mylab.cortisol": {
      markerId: "custom:opaque_id",
      name: "Cortisol (AM)",
      unit: "nmol/L",
      refMin: 170,
      refMax: 720,
      categoryLabel: "My Lab"
    }
  },

  // Per-marker freeform notes — keyed by "category.markerKey".
  // What the marker means to YOU overall, not tied to any one reading.
  markerNotes: {
    "biochemistry.glucose": "Fasted samples only — non-fasted reads run high for me"
  },

  // Per-VALUE freeform notes — keyed by "category.markerKey:YYYY-MM-DD".
  // Context for a specific reading on a specific date (fasting status,
  // retake reason, lab change, etc.). Surfaced inline on the value card +
  // emitted as a dedicated AI-context section. Distinct from markerNotes
  // (overall) and entries.notes (date-level, not marker-specific).
  markerValueNotes: {
    "biochemistry.glucose:2024-03-14": "post-workout, blood draw 30 min after gym",
    "biochemistry.glucose:2024-04-02": "fasted 14h",
    "lipids.ldl:2024-04-02": "retake — first lab reported 5.2 mmol/L"
  },

  // Tombstone-set tracking which (marker, date) values were entered or
  // edited manually (vs imported from a PDF). Same colon-keying as
  // markerValueNotes. Value semantics:
  //   true  = manually added value (no original to revert to)
  //   number = the original SI value (set on first inline edit so a
  //            user can revert later)
  manualValues: {
    "biochemistry.glucose:2024-04-02": true,
    "lipids.ldl:2024-03-15": 3.2
  },

  // Timestamped snapshots of context field changes — appended by recordChange()
  changeHistory: [
    { field: "diet", date: "2026-03-01", snapshot: { type: "carnivore", ... } },
    { field: "stress", date: "2026-02-15", snapshot: { level: "moderate", ... } }
  ],

  // Canonical cross-device reviewed-meal surface. Every image is a bounded
  // thumbnail; full-size selected images never enter importedData.
  nutritionMeals: [
    {
      id: "meal_opaque_id",
      eatenAt: "2026-08-27T18:30:00.000Z",
      mealType: "dinner",
      name: "Lentil bowl",
      components: [{ name: "Cooked lentils", quantityG: 180, nutrients: { proteinG: 16 } }],
      nutrients: { energyKcal: 520, proteinG: 28, carbohydrateG: 64, fatG: 18 },
      source: { kind: "ai-photo-estimate", provider: "...", model: "..." },
      images: [{ thumbnailUrl: "data:image/jpeg;base64,...", width: 240, height: 180 }],
      updatedAt: "2026-08-27T18:34:00.000Z"
    }
  ],
  nutritionTargets: {
    configured: true,
    energyKcal: 2200,
    proteinBasis: "active",
    proteinGPerKg: 1.6,
    widgetNutrients: ["proteinG", "fiberG", "sodiumMg"]
  },
  nutritionContextDays: 30,

  // Agent Access profile-scoped state. Raw setup blobs are never stored;
  // this is the synced state used to reconnect browsers and re-push context.
  agentAccess: {
    enabled: true,
    token: "opaque-read-token",
    contextKey: "base64url-raw-256-bit-key",
    relayProfileId: "profile-id",
    updatedAt: 1782290000000,
    lastPushedAt: 1782290000000
  },
  agentAccessWearableSeriesDays: 30
}
```

### Marker identity and placement compatibility

`markerPlacements` is an additive display map keyed by immutable marker ID. It does not replace the dot keys in `entries`, `refOverrides`, `markerNotes`, `markerValueNotes`, `manualValues`, import snapshots, or provenance maps. Missing or invalid assignments fall back to the marker's native category, while unknown assignments remain stored so a newer runtime can resolve them later.

Custom marker `markerId` is also additive. New definitions receive an opaque ID independent of category and name; legacy profiles receive deterministic IDs during normal profile migration. Profiles without either field remain valid and require no value migration or re-import.

Curated single-profile exports, full database bundles, folder backups, automatic snapshots, encrypted shares, and cross-device sync preserve these fields. See [Marker model and category placement](/developers/marker-model) for placement validation and the mutation boundary.

### Biology Score interpretation records

`importedData.biologyScoreAI` holds per-score summaries, full text, material fingerprints, timestamps and bounded comparison variants. `biologyScoreContextAI` holds context review/suggestions. Preserve both in single-profile exports, backups and Sync; merge variants with `mergeBiologyScoreAIRecords()` rather than replacing the entire record with a stale top-level value. See [Biology Scores persistence](/developers/biology-scores#persistence-and-sync).

### Lab import and range compatibility

`importSnapshots[]` is the historical per-file source of truth; `entries[]` is the current live projection. Active adopted lab ranges are chosen by collection date and then import time. Collection-context ownership is tracked per field so deleting or re-reviewing one same-date report can restore another report's value instead of clearing unrelated context.

Marker moves must migrate both plain dotKeys and date-scoped keys in `manualValues`, `markerValueNotes`, `markerLabels`, and `refOverrides`. See [Lab markers and range internals](/developers/lab-markers-ranges-internals) for precedence, revert, and migration contracts.

### `genetics` — curated Genome data (null if not imported)

```js theme={null}
genetics: {
  source: "AncestryDNA",       // original raw provider or first explicit source
  importDate: "2026-08-10",    // latest autosomal/manual/report update
  coverage: { found: 47, total: 60 },
  catalogVersion: { size: 60, hash: 123456789 },
  apoe: "ε3/ε4",              // resolved from rs429358 + rs7412 when complete

  // Legacy effect counts retained for compatibility, not evidence grades
  effects: { significant: 3, moderate: 8, mild: 9, normal: 25 },

  snps: {
    "rs1801133": {
      genotype: "GA",          // imported/display call
      normalizedGenotype: "AG",// catalog-matched allele/repeat key
      gene: "MTHFR",
      variant: "C677T",
      category: "methylation",
      markers: ["coagulation.homocysteine", "vitamins.folate"],
      effect: "mild",          // legacy internal prioritization field
      valence: "risk",         // association presentation direction
      evidence: {
        level: "strong",
        claimTypes: ["functional", "biomarker_association"],
        scope: "Narrow catalog claim supported by the linked studies.",
        reviewedAt: "2026-08-10"
      },
      relevance: {
        level: "health_context",
        context: "Interpret with measured homocysteine and folate status."
      },
      note: "Catalog interpretation for this matched genotype.",

      // Present only for explicit manual/report overrides
      source: {
        type: "manual",        // or "pdf-report" / "report-text"
        label: "Example report",
        fileName: "example-report.pdf",
        rawText: "bounded matched-row excerpt",
        addedAt: "2026-08-10T10:00:00.000Z"
      }
    }
  },

  mtdna: {
    haplogroup: "U5a",
    confidence: 1,
    matchedMutations: 4,
    totalDiagnostic: 4,
    origin: "European",
    details: "Catalog lineage context",
    coupling: {
      level: "moderately_uncoupled",
      label: "Moderately uncoupled",
      shortLabel: "moderately uncoupled",
      climate: "Cold / Temperate",
      description: "Mitochondrial climate-adaptation context",
      implications: "Interpretation boundary and self-observation context",
      matchedLatBands: [2, 3, 4]
    },
    mutations: ["263G", "750G", "3197C"],
    source: "mtDNA (Living DNA)",
    importDate: "2026-08-10"
  }
}
```

The complete raw DNA or report file is never stored. Raw imports keep only matched catalog calls. Explicit manual/report calls can retain a source label, filename, and a bounded matched-row excerpt.

Raw re-import refreshes raw-file calls but preserves mtDNA and SNP objects with structured `source` metadata. When a raw file and an explicit manual/report override contain the same rsID, the explicit override remains authoritative. `catalogVersion` is preserved when a manual call is added to an older raw import so the app can still prompt for a genuine catalog refresh.

## IndexedDB databases

| Database | Stores | Security and portability |
| - | - | - |
| `labcharts-backups` | Automatic snapshots and optional folder handle | Snapshot contents follow backup filtering; folder handles remain browser-local |
| `getbased-nutrition-{profileId}` | AES-GCM meal payloads plus device-key, summary, comparison, reconciliation, and initialization metadata | Every cached meal is device-key encrypted; full-size images are forbidden; canonical thumbnail-only rows can sync/export through the profile surface |
| `labcharts-wearables-{profileId}` | Daily wearable rows, metadata, non-exportable device key, encrypted OAuth vault records, WHOOP profile sidecar | Raw rows and tokens do not sync; tokens plus WHOOP/Google Health rows are excluded from raw backups |
| Per-profile cycle database | Raw cycle observations and import metadata | Device-local rows; passphrase-wrapped when enabled |
| Cashu wallet database | Proofs and wallet state | Sensitive device-local state with its own export/recovery contract |
| Knowledge Base stores | Local documents, chunks, vectors, and model assets | Device-local/rebuildable according to the Knowledge Base backend |

### Auto-backup database

Database: `labcharts-backups`
Object store: `snapshots`
Key path: `id` (auto-increment)

Each snapshot record:

```js theme={null}
{
  id: 42,                          // auto-increment
  createdAt: "2025-03-15T14:22:00.000Z",
  profileId: "default",
  data: {
    importedData: { ... },         // full importedData object
    unitSystem: "EU",
    rangeMode: "optimal",
    suppOverlayMode: "off",
    noteOverlay: "off",
    chatPersonality: "default",
    chatPersonalityCustom: [...],
    chatPersonalityDeleted: { "custom_old": 1782290000000 },
    // ... all per-profile preferences
    threadIndex: [...],            // chat thread index
    threads: {                     // raw per-thread JSON or ciphertext envelopes
      "t_abc123": "...",
    }
  }
}
```

Maximum 5 snapshots are kept per profile. The oldest is pruned automatically on each new backup.

Unsent `chatDraft_*` records are intentionally absent from full database, auto, folder, and single-profile backups. Sent conversations, their project membership, and custom personas are included. Original chat images are not retained as a recoverable attachment archive; normalized thumbnails can be persisted. Agent executable installations and login state belong to the local agent/Companion and must be reconnected on another machine.

## Encryption and credential envelopes

When passphrase protection is enabled, `crypto.ts` derives one non-exportable AES-256-GCM key with PBKDF2-SHA256, 600,000 iterations, and the global `labcharts-encryption-salt`. Sensitive localStorage values use a passphrase ciphertext envelope. The protected patterns include profile blobs and index, imported data, chat/thread/draft/persona state, provider and voice credentials, Local AI and Knowledge Base secrets, Cashu mnemonic, weather credentials, and extension-declared encrypted keys/prefixes.

Non-sensitive display preferences, selected model IDs, and rebuildable caches remain plaintext. The profile index is not in that group: it is protected when passphrase encryption is on so profile names are not deliberately exposed.

With passphrase protection off, health/profile blobs are normal JSON but supported credentials do not revert to deliberate plaintext storage. They use a `d1:<iv>:<ciphertext>` AES-GCM envelope whose non-exportable key is held in the dedicated device-local wearable database. The storage-key name is included in the authenticated plaintext to prevent envelope substitution. Legacy plaintext credentials are migrated; if the device key cannot be used, persistence fails closed and the value remains memory-only.

Wearable OAuth credentials use a stricter per-profile vault under the same device-key primitive. Credential generations prevent an in-flight refresh from recreating a disconnected account. These records and keys are excluded from sync and backups.

Nutrition cached meal payloads are always wrapped with a per-profile non-extractable device key. `sanitizeNutritionMeal()` strips full images and allowlists up to four bounded thumbnails before the record reaches IndexedDB, the profile mirror, Sync, or export.

Raw WHOOP and Google Health daily rows are always wrapped with the device key and excluded from raw-data backups. WHOOP-owned connection metadata, source summaries, metrics/overrides, and change events are split into the encrypted `whoop-profile-data:v1` sidecar before the ordinary imported profile blob is stored, then rehydrated for runtime use. Other wearable and cycle rows receive an additional passphrase wrapper only when passphrase protection is enabled.

`BroadcastChannel('labcharts-sync')` carries only `{ type: 'data-changed', profileId }`. It never distributes a derived key; each tab maintains and clears its own in-memory unlock state.

Full database/folder backups omit `d1:` credentials because their key is not portable. When passphrase protection is enabled, supported passphrase-encrypted credentials can be retained and require the same salt/passphrase after restore. Wearable OAuth tokens remain excluded in both modes.

## Hosted encrypted share records

Profile sharing is opt-in and uses hosted storage only after the user confirms upload in the Share Profile modal. The browser creates a normal v2 single-profile export, strips credential surfaces through `buildClientExportObject()`, compresses it when supported, and encrypts it locally before upload. The share password never leaves the browser and is not stored in the share link.

Current official `vps1_...` records are sent directly to the dedicated profile-share service and stored in its bounded SQLite object store. The database also contains expiry/deletion maintenance records and rotating HMAC-derived abuse-control identifiers based on caller network metadata. It does not store the raw share password or decrypted profile. Cleanup runs at startup, hourly, after creation, and on expired reads; the operated database has no retained backup.

The Vercel `api/share.ts` path is now a bounded transition adapter for older Blob records. Before the configured cutover it can use the legacy store; during the no-more-than-31-day transition it redirects new writes, serves live old reads/deletes, and redirects misses; after the deadline it redirects all supported requests. Existing envelopes are not migrated or copied. Local development uses the same logical record shape in a process-local map.

```js theme={null}
{
  id: "base64url-share-id",
  createdAt: "2026-06-02T12:00:00.000Z",
  expiresAt: "2026-06-09T12:00:00.000Z",
  manageTokenHash: "sha256-hex-of-local-management-token",
  envelope: {
    schema: "getbased-profile-share",
    version: 1,
    createdAt: "2026-06-02T12:00:00.000Z",
    expiresAt: "2026-06-09T12:00:00.000Z",
    kdf: {
      name: "PBKDF2",
      hash: "SHA-256",
      iterations: 600000,
      salt: "base64url"
    },
    cipher: {
      name: "AES-GCM",
      iv: "base64url"
    },
    compression: "gzip", // or "none"
    ciphertext: "base64url"
  }
}
```

The remote record contains only ciphertext plus metadata needed to decrypt it in a recipient's browser. The local active-link record contains the management token so the creating browser can delete the hosted envelope later; losing that browser-local record means the modal can no longer stop that link directly.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.