How Supermemory Manages State: Zustand, IndexedDB, and Per-Project Persistence
Supermemory leverages Zustand with custom IndexedDB middleware to persist per-project chat histories while using scoped hooks and URL-driven state to isolate data between workspaces.
Supermemory is an open-source AI memory platform built with modern React architecture. Understanding how Supermemory manages state reveals a sophisticated approach to client-side persistence that balances performance, type safety, and offline capability through Zustand stores and a custom IndexedDB adapter.
Core State Management with Zustand
Supermemory’s state layer is built on Zustand, a lightweight, unopinionated React state-management library. Unlike Redux, Zustand requires no reducers or action creators, enabling rapid development while maintaining full TypeScript support.
The architecture centers on apps/web/stores/chat.ts, which defines the primary persistent store for conversation data using Zustand’s create function combined with the persist middleware.
Persistent Chat State Architecture
The Per-Project Store Structure
The usePersistentChatStore creates a Zustand store containing a byProject Map that keys conversation data by project identifier. Each entry stores:
currentChatId: The active conversation identifierconversations: An array of records containing rawUIMessage[]arrays from the AI SDK, optional titles, andlastUpdatedtimestamps
This structure ensures complete isolation between projects—chat histories never leak across workspaces because every lookup requires a projectId key.
Custom IndexedDB Persistence Layer
Persistence is implemented through Zustand’s persist middleware configured with a custom storage adapter. In apps/web/stores/indexeddb-storage.ts, the indexedDBStorage adapter wraps IndexedDB operations via createJSONStorage.
This approach provides several advantages over localStorage:
- Larger storage quotas for extensive AI conversation histories without hitting 5MB browser limits
- Asynchronous I/O that doesn't block the main thread during serialization
- Automatic migration from legacy localStorage data when users upgrade from earlier versions
- Offline capability with immediate data availability on page reload
Deep Equality Optimization
To prevent unnecessary re-renders when the AI SDK emits identical message arrays during streaming, the store implements areUIMessageArraysEqual. This deep-equality check runs before any state update, ensuring React components only re-render when message content actually changes rather than on every reference update.
Project Scoping and URL Synchronization
The useProject Hook
Located in apps/web/stores/index.ts, the useProject hook reads the URL query parameter project= using nuqs (Next.js URL Query State library). It resolves to a specific project ID or falls back to DEFAULT_PROJECT_ID, ensuring every store operation knows its workspace context.
This URL-driven approach enables:
- Sharable links that open specific projects directly
- Browser history integration for back/forward navigation between projects
- Cross-component synchronization without prop drilling or context providers
Container Tags for Backend Context
The hook also computes effectiveContainerTags—"Nova Space" identifiers that the UI sends to backend API calls. This computed state ensures the frontend and backend maintain consistent project context for every request, scoped to the correct logical workspace.
Transient UI State Stores
Not all state requires persistence. Supermemory maintains in-memory-only stores for ephemeral UI data that should not survive page reloads.
Graph View Highlights
The useGraphHighlightsStore in apps/web/stores/highlights.ts tracks highlighted document IDs in the graph visualization. It stores an array of IDs with a lastUpdated timestamp, enabling selective re-renders only when the highlighted set changes. This store is non-persistent by design, as highlight selections are temporary user interface states.
Quick Note Drafts
The useQuickNoteDraftStore in apps/web/stores/quick-note-draft.ts maintains per-project draft strings for the quick-note editor. While scoped to project IDs like the chat store, this data remains in-memory only, persisting unsent content during navigation between projects but clearing on full page reload.
Hook Façade Pattern
Supermemory abstracts raw Zustand stores behind developer-friendly hooks that inject project context automatically. The usePersistentChat() hook in apps/web/stores/chat.ts exposes a memoized API:
setCurrentChatIdsetConversationdeleteConversationsetConversationTitlegetCurrentConversationgetCurrentChat
These hooks automatically inject the projectId from useProject, eliminating manual project ID management. Similarly, useGraphHighlights() and useQuickNoteDraft(projectId) provide type-safe interfaces to their respective stores while handling scoping concerns internally.
Practical Implementation Examples
Accessing Persistent Chat Data
import { usePersistentChat } from "@/stores/chat"
function ChatPanel({ chatId }: { chatId: string }) {
const {
currentChatId,
conversations,
setConversation,
getCurrentConversation,
} = usePersistentChat()
const messages = getCurrentConversation() ?? []
const sendMessage = async (content: string) => {
const newMessages = [...messages, { id: crypto.randomUUID(), role: "user", content }]
await setConversation(chatId, newMessages) // persisted in IndexedDB
}
return (
<div>
<h2>{conversations.find(c => c.id === chatId)?.title ?? "Untitled"}</h2>
{/* render messages */}
<button onClick={() => sendMessage("Hello!")}>Send</button>
</div>
)
}
(Source: apps/web/stores/chat.ts)
Managing Graph Highlights
import { useGraphHighlights } from "@/stores/highlights"
function GraphToolbar() {
const { documentIds, setDocumentIds, clear } = useGraphHighlights()
const toggle = (docId: string) => {
setDocumentIds(documentIds.includes(docId)
? documentIds.filter(id => id !== docId)
: [...documentIds, docId])
}
return (
<>
<button onClick={clear}>Clear highlights</button>
{/* UI that calls toggle(docId) */}
</>
)
}
(Source: apps/web/stores/highlights.ts)
Handling Quick Note Drafts
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
function QuickNote({ projectId }: { projectId: string }) {
const { draft, setDraft, resetDraft } = useQuickNoteDraft(projectId)
return (
<textarea
value={draft}
onChange={e => setDraft(e.target.value)}
placeholder="Write a quick note..."
/>
<button onClick={resetDraft}>Reset</button>
)
}
(Source: apps/web/stores/quick-note-draft.ts)
Summary
- Zustand powers all state management with minimal boilerplate compared to Redux-style architectures
- IndexedDB persistence via the custom
indexedDBStorageadapter inapps/web/stores/indexeddb-storage.tsensures chat histories survive page reloads and handle large AI message payloads - Per-project isolation through the
byProjectMap structure prevents data leakage between workspaces - Deep equality checks using
areUIMessageArraysEqualoptimize rendering performance for high-frequency AI message streams - URL-driven state via
useProjectinapps/web/stores/index.tssynchronizes project selection with the browser address bar usingnuqs - Hook façades abstract store complexity while maintaining type safety and automatic project scoping
Frequently Asked Questions
Why does Supermemory use IndexedDB instead of localStorage for chat persistence?
IndexedDB provides significantly larger storage quotas and asynchronous operations essential for storing extensive AI conversation histories. The custom indexedDBStorage adapter in apps/web/stores/indexeddb-storage.ts also handles automatic migration from legacy localStorage data while supporting offline functionality that survives browser restarts.
How does Supermemory ensure chat history stays isolated between projects?
The usePersistentChatStore organizes data in a byProject Map structure where each project ID keys its own conversation records. The usePersistentChat() hook automatically scopes all operations to the current projectId derived from the URL query parameter, ensuring complete logical separation without manual namespace management.
What prevents unnecessary re-renders when AI messages update?
The store implements areUIMessageArraysEqual to perform deep equality checks on UIMessage[] arrays before state updates in apps/web/stores/chat.ts. When the AI SDK emits identical message payloads during streaming, Zustand skips the update, preventing React component re-renders and improving UI responsiveness.
Where is project selection state stored and how does it sync across components?
Project selection lives in the URL query string (project=) managed by nuqs in the useProject hook at apps/web/stores/index.ts. This approach synchronizes project context across all components without prop drilling, enables bookmarkable project views, and ensures stores like usePersistentChat automatically scope to the correct workspace based on the current URL.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →