# How Supermemory Manages State: Zustand, IndexedDB, and Per-Project Persistence

> Discover how Supermemory manages state using Zustand and IndexedDB for per-project persistence. Explore scoped hooks and URL-driven state for workspace isolation.

- Repository: [supermemory/supermemory](https://github.com/supermemoryai/supermemory)
- Tags: internals
- Published: 2026-03-25

---

**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`](https://github.com/supermemoryai/supermemory/blob/main/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 identifier
- `conversations`: An array of records containing raw **`UIMessage[]`** arrays from the AI SDK, optional titles, and `lastUpdated` timestamps

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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/apps/web/stores/chat.ts) exposes a memoized API:

- `setCurrentChatId`
- `setConversation`
- `deleteConversation`
- `setConversationTitle`
- `getCurrentConversation`
- `getCurrentChat`

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

```tsx
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`](https://github.com/supermemoryai/supermemory/blob/main/apps/web/stores/chat.ts))*

### Managing Graph Highlights

```tsx
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`](https://github.com/supermemoryai/supermemory/blob/main/apps/web/stores/highlights.ts))*

### Handling Quick Note Drafts

```tsx
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`](https://github.com/supermemoryai/supermemory/blob/main/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 `indexedDBStorage` adapter in [`apps/web/stores/indexeddb-storage.ts`](https://github.com/supermemoryai/supermemory/blob/main/apps/web/stores/indexeddb-storage.ts) ensures chat histories survive page reloads and handle large AI message payloads
- **Per-project isolation** through the `byProject` Map structure prevents data leakage between workspaces
- **Deep equality checks** using `areUIMessageArraysEqual` optimize rendering performance for high-frequency AI message streams
- **URL-driven state** via `useProject` in [`apps/web/stores/index.ts`](https://github.com/supermemoryai/supermemory/blob/main/apps/web/stores/index.ts) synchronizes project selection with the browser address bar using `nuqs`
- **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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/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`](https://github.com/supermemoryai/supermemory/blob/main/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.