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)
Per-profile keys
importedData structure
Stored as JSON atlabcharts-{profileId}-imported. Full database and folder backups preserve this blob, while curated single-profile exports select user-facing fields from it.
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 for placement validation and the mutation boundary.
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 for precedence, revert, and migration contracts.
genetics — curated Genome data (null if not imported)
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
Auto-backup database
Database:labcharts-backups
Object store: snapshots
Key path: id (auto-increment)
Each snapshot record:
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.js 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 throughbuildClientExportObject(), 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.js 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.