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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj:stream-filters-v1 | local | components/filters/StreamFilterPanel.tsx | same | Stream view's chip filters (year/month/notebook/visibility/sort). Hook: usePersistedStreamFilters. | none |
hj:highlights-filters-v1 | local | app/dashboard/highlights/page.tsx | same | Highlights wall: filterQuery + colorFilter + sortBy. | none |
hj:admin-seasons-filters-v1 | local | app/dashboard/admin/seasons/page.tsx | same | Admin Story Seasons list: search + status + mode + owner + publish + sort. | none |
hj:notebooks-filters-v1 | local | app/dashboard/notebooks/page.tsx | same | Notebooks 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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj-notebooks-list-v1 | local | hooks/useNotebooks.ts | same (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-v1 | session | app/dashboard/stream/page.tsx | same | Capped slice (50) of stream entries for instant scroll. Invalidated on quote-share write. | 30 min |
hj-life-map-v1 | session | hooks/useLifeMap.ts | same | Life-map graph snapshot for instant first paint. | none |
hj-weekly-reviews-v1 | session | hooks/useWeeklyReviews.ts | same | Prior weekly-review list so the page paints before /api responds. staleTime: 60s on the TanStack side. | none |
hj:family-today | local | hooks/useFamily.ts | same | Today's family snapshot (members + drops). _ts timestamp inside the blob. | 5 min |
hj:comics-cache | local | app/dashboard/comics/page.tsx | same | Comics list for instant paint on revisit. _ts timestamp inside the blob. | none enforced (always refetches in background) |
hj:tasks-cache | local | hooks/useUserTasks.ts | same | User tasks list. _ts timestamp inside the blob. | 10 min |
hj:routines-cache | local | hooks/useUserTasks.ts | same | User 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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj-navbar-snapshot | local | components/layout/Navbar.tsx | same + 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-count | local | app/dashboard/notebooks/page.tsx | same | Count of notebooks so the index renders the right number of skeleton cards. | none |
hj-orphan-exists | local | app/dashboard/notebooks/page.tsx | same | Boolean (0/1) — whether to render the "Other" tile skeleton. | none |
hj-notebook-entries-count:<id> | local | components/notebooks/EntrySkeleton.tsx | same | Per-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-count | local | app/dashboard/page.tsx | same | Cached 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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj-widgets-collapsed | local | app/dashboard/page.tsx | same | Dashboard sidebar widget panel collapsed/expanded. Stored as '1' / '0'. | none |
hj-writing-prompts-hidden | local | components/journal/RotatingPromptCloud.tsx | same | Whether the rotating-prompt cloud is hidden. '1' / '0'. | none |
hj-writing-prompt-topics | local | same as above | same | User-curated list of prompt topics (JSON array of strings). | none |
hj-writing-prompts-show-defaults | local | same as above | same | Whether to surface default topics alongside the user's curated set. '1' / '0'. | none |
hj:audio-playback-rate:v1 | local | app/seasons/[id]/SeasonClient.tsx | same | Listener's preferred audio/video playback speed (one of `0.75 | 1 |
hj:audio-volume:v1 | local | app/seasons/[id]/SeasonClient.tsx + components/seasons/ChapterPlaylistPlayer.tsx | same | Listener'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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj:story-progress:<storyId>:v1 | local | components/notebooks/GraphicNovelReader.tsx | same | Current 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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj-season-create-draft | local | app/dashboard/admin/seasons/page.tsx | same | In-progress "Create Season" form fields — survives accidental navigation away from the admin panel. | none |
hj:sj-anderson-podcast-submissions | local | app/sj-anderson/SJAndersonClient.tsx | same | Podcast 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.
| Key | Realm | Set in | Read in | Purpose | TTL |
|---|---|---|---|---|---|
hj:is-super-admin | local | lib/useAdminStatus.ts | same | Cached super-admin boolean + the user id it was computed for. Cleared on sign-out. | 10 min |
hj:feature-flags | local | lib/useFeatureFlags.ts | same | Last-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.
| Key | Realm | Where | Purpose |
|---|---|---|---|
theme, customColors, headerBackground, streamSettings | local | contexts/ThemeContext.tsx | Theme + visual settings |
dashboardCachedEntries, dashboardCachedOffset, dashboardCachedHasMore, dashboardCachedAt | session | app/dashboard/page.tsx | Dashboard entries page snapshot (30 min TTL) |
landing_page_variant | session | app/page.tsx + signup | A/B variant assignment for funnel attribution |
open-energy-seen-badges, open-energy-synthesis-revealed | local | app/open-energy/experiments/page.tsx | First-view dismissals |
cafe-watch-client-id, cafe-watch-theater | session/local | app/write-cafe/battles/watch/page.tsx | Battle-watch client id + theater toggle |
spotify_code_verifier, spotify_state | session | app/auth/spotify/callback/page.tsx | Spotify OAuth PKCE flow |
Adding a new key
- Pick a name in the form
hj:<surface>-<purpose>-v<n>(colon, not hyphen). - Pick the realm —
sessionStorageif it shouldn't survive tab close (caches that can re-fetch cheaply),localStorageotherwise. - Guard reads + writes with
typeof window !== 'undefined'andtry/catch(private mode + quota errors). - Add a row above. If the key fits an existing category, drop it in that table; if not, add a new category section.
- If it's a filter blob, follow
the persistence pattern.