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 subscriptionsBlobFrontend– Manages binary asset storage and retrievalAwarenessFrontend– Controls real-time presence and cursor synchronizationIndexerFrontend– 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:
- Request –
nbstoreService.openStore(key, options)receives a unique store key (e.g.,workspace:local:doc-id) and initialization options - Worker Instantiation – The provider creates a
MessageChanneland spins up (or connects to) aSharedWorker - Client Return – The call returns
{store, dispose}, wherestoreis aStoreClientinstance anddisposeis 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
NbstoreProviderandNbstoreServicehandle dependency injection and store lifecycle management - Multiple backends (IndexedDB, SQLite, Cloud) plug into the same interface, enabling platform-specific optimizations
- All operations route through
MessageChannelto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →