Skip to main content

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 at labcharts-{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)

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 — auto-backup

Database: labcharts-backups Object store: snapshots Key path: id (auto-increment) Each snapshot record:
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 and custom personas are included.

Encryption

When encryption is enabled (labcharts-encryption-enabled = 'true'), sensitive localStorage keys are stored as AES-256-GCM ciphertext (base64-encoded) instead of plaintext JSON. Encrypted key patterns (SENSITIVE_PATTERNS in crypto.js):
  • *-imported — all importedData
  • *-chat — legacy chat history
  • *-chat-threads — thread index
  • *-chat-t_* — per-thread messages
  • *-chatDraft_* — device-local unsent drafts
  • *-chatPersonalityCustom — custom persona records
  • *-chatPersonalityDeleted — custom persona tombstones
Keys that are not sensitive (preferences, model selections, pricing caches, profile index) are always stored in plaintext. The encryption key is derived via PBKDF2 from a user passphrase + per-profile salt (labcharts-{profileId}-enc-salt).

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. Production records are stored by api/share.js in private Vercel Blob objects under profile-shares/v1/{id}.json. Production also stores small per-client fixed-slot rate-limit marker records under profile-share-rate/v1/{sha256-client}/{windowStart}/{slot}.json to add friction to anonymous share creation without relying on a read-then-write counter. Local development stores the same logical share record in the dev-server.js in-memory map.
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.