What is nbstore in AFFiNE? Complete Architecture and Implementation Guide

nbstore is a WebWorker-based storage abstraction layer that unifies document persistence, blob storage, real-time awareness, and full-text indexing across multiple backends in the AFFiNE workspace.

nbstore (short for Notebook Store) serves as the core data-layer abstraction throughout the AFFiNE frontend. It isolates the UI layer from underlying persistence mechanisms while supporting real-time collaboration, offline-first behavior, and multi-backend flexibility through a standardized API.

Core Architecture of nbstore

The nbstore system delegates all persistence concerns to a dedicated worker thread, exposing a clean client interface that the rest of the application consumes.

NbstoreProvider and Service Layer

The NbstoreProvider declares the contract for opening a store at packages/frontend/core/src/modules/storage/providers/nbstore.ts. It creates a SharedWorker (or dedicated worker) and returns a StoreClient instance alongside a disposal function.

The NbstoreService acts as a thin façade injected via the DI container. Located at packages/frontend/core/src/modules/storage/services/nbstore.ts, it forwards openStore calls to the injected provider, making the storage layer swappable across different application contexts.

StoreClient and Frontend Abstractions

At packages/common/nbstore/src/worker/client.ts, the StoreManagerClient and StoreClient manage worker connections via MessageChannel. The StoreClient exposes four high-level frontends:

  • DocFrontend – Handles document CRUD, updates, and subscriptions
  • BlobFrontend – Manages binary asset storage and retrieval
  • AwarenessFrontend – Controls real-time presence and cursor synchronization
  • IndexerFrontend – Provides full-text search capabilities

Workspace Engine Integration

The Workspace Engine consumes NbstoreService to obtain a StoreClient, as implemented in packages/frontend/core/src/modules/workspace/entities/engine.ts. The engine accesses frontends through properties like engine.doc and engine.blob to drive UI state, synchronization, and indexing without directly managing storage implementation details.

How nbstore Works: The Data Flow

Understanding the initialization and lifecycle of an nbstore connection reveals how AFFiNE achieves storage isolation.

Opening a Store Connection

When a workspace initializes, the engine calls the service layer to establish a persistent connection:

  1. Request – nbstoreService.openStore(key, options) receives a unique store key (e.g., workspace:local:doc-id) and initialization options
  2. Worker Instantiation – The provider creates a MessageChannel and spins up (or connects to) a SharedWorker
  3. Client Return – The call returns {store, dispose}, where store is a StoreClient instance and dispose is a cleanup function

Runtime Operations

Once connected, all reads and writes flow through the worker:

  • Document updates route through docFrontend.pushDocUpdate()
  • Binary assets upload via blobFrontend.set()
  • Real-time awareness broadcasts through awarenessFrontend
  • Search indexing occurs asynchronously via indexerFrontend

The worker handles offline caching, sync-conflict resolution, and telemetry transparently to the UI layer.

Practical Implementation Examples

Opening a Workspace Store

As implemented in packages/frontend/core/src/modules/workspace/entities/engine.ts, the WorkspaceEngine initializes storage during its start() method:

// inside WorkspaceEngine.start()
const { store, dispose } = this.nbstoreService.openStore(
  `${this.props.isSharedMode ? 'shared:' : ''}workspace:${flavour}:${workspaceId}`,
  this.props.engineWorkerInitOptions
);

// Optional: enable battery-save mode for non-local workspaces
if (featureFlagService.flags.enable_battery_save_mode.value && flavour !== 'local') {
  store.enableBatterySaveMode().catch(console.error);
}

// Keep a reference for later use
this.client = store;
this.disposables.push(dispose);

Source: packages/frontend/core/src/modules/workspace/entities/engine.ts (lines 61-66)

Fetching Documents with DocFrontend

Retrieve a document's Yjs state and metadata through the store client:

async function loadDoc(client: StoreClient, docId: string) {
  const docRecord = await client.docFrontend.getDoc(docId);
  // `docRecord` contains the serialized Yjs state and metadata
  return docRecord;
}

Source: StoreClient definition in packages/common/nbstore/src/worker/client.ts (lines 48-50)

Storing Binary Assets

Upload images or attachments via the BlobFrontend:

async function putBlob(client: StoreClient, key: string, data: Uint8Array) {
  await client.blobFrontend.set({
    key,
    value: data,
    mime: 'image/png',
    // optional metadata …
  });
}

Source: BlobFrontend usage in packages/common/nbstore/src/worker/client.ts (lines 49-50)

Subscribing to Real-Time Updates

Listen for document changes to synchronize UI state:

client.docFrontend.subscribeDocUpdate((record, origin) => {
  console.log('Doc updated:', record.guid, 'origin:', origin);
});

Source: subscribeDocUpdate implementation in packages/common/nbstore/src/worker/client.ts (lines 13-22)

Graceful Store Teardown

When disposing a workspace, clean up resources to prevent memory leaks:

// When the workspace is disposed
dispose();           // called by WorkspaceEngine via its disposables
client.dispose();    // optional extra cleanup if you kept the StoreClient

Source: The dispose function returned from NbstoreProvider.openStore in packages/frontend/core/src/modules/storage/providers/nbstore.ts (lines 20-22)

Supported Storage Backends

nbstore achieves backend flexibility by plugging different implementations into the same worker API:

  • IndexedDB (packages/frontend/nbstore/idb) – Browser-based persistent storage for web clients
  • SQLite (packages/frontend/nbstore/sqlite) – Native file-based storage for Electron builds
  • Cloud (packages/frontend/nbstore/cloud) – Remote synchronization endpoints
  • BroadcastChannel (packages/frontend/nbstore/broadcast-channel) – Cross-tab communication for multi-instance scenarios

Worker entry points at packages/frontend/apps/*/nbstore.worker.ts instantiate the appropriate backend based on the application context (web, mobile, or desktop).

Summary

  • nbstore (Notebook Store) is the unified persistence abstraction in AFFiNE's frontend architecture
  • It isolates storage concerns into WebWorkers, exposing clean frontends (DocFrontend, BlobFrontend, etc.) to the UI layer
  • The NbstoreProvider and NbstoreService handle dependency injection and store lifecycle management
  • Multiple backends (IndexedDB, SQLite, Cloud) plug into the same interface, enabling platform-specific optimizations
  • All operations route through MessageChannel to the worker, supporting offline-first behavior and real-time collaboration

Frequently Asked Questions

What does nbstore stand for in AFFiNE?

(nbstore)[https://github.com/toeverything/AFFiNE/tree/canary/packages/common/nbstore] stands for Notebook Store. It is the internal codename for the storage abstraction layer that manages all document persistence, binary assets, and real-time awareness data within the AFFiNE application.

How does nbstore differ from direct database access?

Rather than accessing IndexedDB or SQLite directly, nbstore routes all storage operations through a WebWorker using MessageChannel ports. This architecture prevents blocking the main thread during heavy I/O operations, enables background synchronization, and allows the UI layer to remain agnostic about whether data comes from local cache or cloud storage.

Can I use nbstore outside of AFFiNE's WorkspaceEngine?

While nbstore is primarily consumed through the WorkspaceEngine at packages/frontend/core/src/modules/workspace/entities/engine.ts, you can instantiate it independently by injecting NbstoreService into your own module. Call openStore() with a unique key and options to receive a StoreClient with full access to documents, blobs, and indexing capabilities.

What happens when I call dispose() on a store?

Calling dispose() (returned by NbstoreProvider.openStore or via client.dispose()) closes the MessageChannel, terminates the worker connection, and releases all frontend references. This cleanup is essential for preventing memory leaks when switching workspaces or closing document tabs in AFFiNE's interface.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →