reference

localStorage / sessionStorage key inventory

Catalog of every hj:* / hj-* storage key in the frontend (apps/frontend/src/). Backend and the Chrome extension run in different storage realms and are out of scope here.

This is an inventory, not a migration plan — existing keys are recorded as-is. Naming convention for new keys: prefer hj:<surface>-<purpose>-v<n> going forward (colon separator distinguishes from older hj-… keys). Flagged inconsistencies are called out per row but not renamed.

For the design pattern behind filter-state keys (the *-filters-v1 family), see docs/ai/CONVENTIONS.md → Filter persistence pattern.

How keys are grouped

flowchart LR
    A[hj* keys] --> B[Filter persistence<br/>hj:*-filters-v1]
    A --> C[Cache snapshots<br/>for instant first paint]
    A --> D[UI preferences]
    A --> E[Skeleton-sizing counts]
    A --> F[Draft state]
    A --> G[Per-feature flags / auth]

Each row below names the storage realm (local = localStorage, persists across browser sessions; session = sessionStorage, cleared on tab close).


Filter persistence

Two-effect hydrate/write pattern documented in CONVENTIONS.md. Always local, always JSON blob, always per-field validated on read.

KeyRealmSet inRead inPurposeTTL
hj:stream-filters-v1localcomponents/filters/StreamFilterPanel.tsxsameStream view's chip filters (year/month/notebook/visibility/sort). Hook: usePersistedStreamFilters.none
hj:highlights-filters-v1localapp/dashboard/highlights/page.tsxsameHighlights wall: filterQuery + colorFilter + sortBy.none
hj:admin-seasons-filters-v1localapp/dashboard/admin/seasons/page.tsxsameAdmin Story Seasons list: search + status + mode + owner + publish + sort.none
hj:notebooks-filters-v1localapp/dashboard/notebooks/page.tsxsameNotebooks index: filterQuery + typeFilter + sortBy.none

Cache snapshots (instant first paint)

JSON blobs primed into TanStack Query via queryClient.setQueryData in a post-mount effect. See CONVENTIONS.md → Progressive loading.

KeyRealmSet inRead inPurposeTTL
hj-notebooks-list-v1localhooks/useNotebooks.tssame (usePrimeNotebooksFromCache)Full notebooks list snapshot so the index grid paints with real titles + covers before the network responds.none (refreshed on every fetch)
hj-stream-cache-v1sessionapp/dashboard/stream/page.tsxsameCapped slice (50) of stream entries for instant scroll. Invalidated on quote-share write.30 min
hj-life-map-v1sessionhooks/useLifeMap.tssameLife-map graph snapshot for instant first paint.none
hj-weekly-reviews-v1sessionhooks/useWeeklyReviews.tssamePrior weekly-review list so the page paints before /api responds. staleTime: 60s on the TanStack side.none
hj:family-todaylocalhooks/useFamily.tssameToday's family snapshot (members + drops). _ts timestamp inside the blob.5 min
hj:comics-cachelocalapp/dashboard/comics/page.tsxsameComics list for instant paint on revisit. _ts timestamp inside the blob.none enforced (always refetches in background)
hj:tasks-cachelocalhooks/useUserTasks.tssameUser tasks list. _ts timestamp inside the blob.10 min
hj:routines-cachelocalhooks/useUserTasks.tssameUser routines list. _ts timestamp inside the blob.10 min

Skeleton-sizing counts

Tiny integer values consulted before the real fetch lands so the skeleton grid is the right shape on first paint.

KeyRealmSet inRead inPurposeTTL
hj-navbar-snapshotlocalcomponents/layout/Navbar.tsxsame + app/dashboard/notebooks/page.tsx (optimistic userId seed)Last-known signed-in user (id + email + avatar) so the navbar avatar paints before Supabase session resolves. Cleared on sign-out.none
hj-notebooks-countlocalapp/dashboard/notebooks/page.tsxsameCount of notebooks so the index renders the right number of skeleton cards.none
hj-orphan-existslocalapp/dashboard/notebooks/page.tsxsameBoolean (0/1) — whether to render the "Other" tile skeleton.none
hj-notebook-entries-count:<id>localcomponents/notebooks/EntrySkeleton.tsxsamePer-notebook entry-count cache so the detail page renders the right number of entry skeletons. Prefix exported as ENTRY_COUNT_PREFIX.none
hj-dashboard-entries-countlocalapp/dashboard/page.tsxsameCached entry count for the main dashboard's skeleton sizing. Default 4 for first-time visitors.none

UI preferences (per-browser)

Pure UI state — boolean toggles, hidden lists, expand/collapse — that's a nicety to remember but not load-bearing.

KeyRealmSet inRead inPurposeTTL
hj-widgets-collapsedlocalapp/dashboard/page.tsxsameDashboard sidebar widget panel collapsed/expanded. Stored as '1' / '0'.none
hj-writing-prompts-hiddenlocalcomponents/journal/RotatingPromptCloud.tsxsameWhether the rotating-prompt cloud is hidden. '1' / '0'.none
hj-writing-prompt-topicslocalsame as abovesameUser-curated list of prompt topics (JSON array of strings).none
hj-writing-prompts-show-defaultslocalsame as abovesameWhether to surface default topics alongside the user's curated set. '1' / '0'.none
hj:audio-playback-rate:v1localapp/seasons/[id]/SeasonClient.tsxsameListener's preferred audio/video playback speed (one of `0.751
hj:audio-volume:v1localapp/seasons/[id]/SeasonClient.tsx + components/seasons/ChapterPlaylistPlayer.tsxsameListener's preferred audio/video volume (number in [0, 1] — the HTMLMediaElement.volume range), persisted across sessions and shared between the journal-mode <audio>/<video> player and the novel-mode chapter playlist. Written from onVolumeChange on the media element; hydrated as null so the browser default applies on first render.none

Resume state (reader progress)

Position tracking for resumable content — cleared on completion so re-opening a finished story starts fresh.

KeyRealmSet inRead inPurposeTTL
hj:story-progress:<storyId>:v1localcomponents/notebooks/GraphicNovelReader.tsxsameCurrent panel index in graphic-novel reader. One key per story; <storyId> is the notebook id. Cleared on story completion.indefinite (or until completion)

Draft state (in-flight authoring)

User input that would be lost on accidental navigation. Cleared after successful submission.

KeyRealmSet inRead inPurposeTTL
hj-season-create-draftlocalapp/dashboard/admin/seasons/page.tsxsameIn-progress "Create Season" form fields — survives accidental navigation away from the admin panel.none
hj:sj-anderson-podcast-submissionslocalapp/sj-anderson/SJAndersonClient.tsxsamePodcast submission checklist state — memory aid for the writer, not a server-tracked status.none

Feature flags / auth

Cached server-derived booleans that gate UI surfaces.

KeyRealmSet inRead inPurposeTTL
hj:is-super-adminlocallib/useAdminStatus.tssameCached super-admin boolean + the user id it was computed for. Cleared on sign-out.10 min
hj:feature-flagslocallib/useFeatureFlags.tssameLast-known feature flag map so the dashboard doesn't wait on a round-trip before rendering. Survives logout intentionally (flags rarely flip per-user).none

Out-of-pattern keys (legacy / non-prefixed)

Not hj* but live in the same storage realm; included so a future audit doesn't re-discover them. These keys predate the convention and should be migrated to hj:* if/when touched.

KeyRealmWherePurpose
theme, customColors, headerBackground, streamSettingslocalcontexts/ThemeContext.tsxTheme + visual settings
dashboardCachedEntries, dashboardCachedOffset, dashboardCachedHasMore, dashboardCachedAtsessionapp/dashboard/page.tsxDashboard entries page snapshot (30 min TTL)
landing_page_variantsessionapp/page.tsx + signupA/B variant assignment for funnel attribution
open-energy-seen-badges, open-energy-synthesis-revealedlocalapp/open-energy/experiments/page.tsxFirst-view dismissals
cafe-watch-client-id, cafe-watch-theatersession/localapp/write-cafe/battles/watch/page.tsxBattle-watch client id + theater toggle
spotify_code_verifier, spotify_statesessionapp/auth/spotify/callback/page.tsxSpotify OAuth PKCE flow

Adding a new key

  1. Pick a name in the form hj:<surface>-<purpose>-v<n> (colon, not hyphen).
  2. Pick the realm — sessionStorage if it shouldn't survive tab close (caches that can re-fetch cheaply), localStorage otherwise.
  3. Guard reads + writes with typeof window !== 'undefined' and try/catch (private mode + quota errors).
  4. Add a row above. If the key fits an existing category, drop it in that table; if not, add a new category section.
  5. If it's a filter blob, follow the persistence pattern.
LOCAL STORAGE — Docs | HiveJournal