Data Sources
Data sources are the bridge between your backend and the library. There are two interfaces: CollectionSource (fetches schema and view metadata) and DocumentSource (CRUD for rows). You pass both to useCollectionManager or initializeCollection, and the library handles the rest.
Source Interfaces
Section titled “Source Interfaces”interface CollectionSource { fetch(collectionId: string): Promise<CollectionJSON>; update(ctx: CollectionContext, changes: Partial<CollectionJSON>): Promise<void>; subscribe?: ( collectionId: string, applyRemote: (events: CollectionSourceEvent[]) => void, ) => () => void;}
interface DocumentSource { fetch(collectionId: string, params: FetchDocumentsParams): Promise<PageResult<DocumentJSON>>; create(ctx: { collectionId: string; document: Document }): Promise<void>; update(ctx: DocumentContext, changes: Partial<DocumentJSON>): Promise<void>; delete(ctx: { collectionId: string; documentId: string }): Promise<void>; subscribe?: ( collectionId: string, applyRemote: (events: DocumentSourceEvent[]) => void, ) => () => void;}The subscribe method is optional. Implement it to enable real-time updates from other clients.
In-Memory Sources
Section titled “In-Memory Sources”The simplest option. Data lives in memory — no backend, no network. Good for demos, tests, and prototyping.
import { InMemoryCollectionSource, InMemoryDocumentSource,} from "@blocknote/block-view/core/sources/in-memory";
const collectionSource = new InMemoryCollectionSource({ id: "tasks", name: "Tasks", schema: { title: { id: "title", collectionId: "tasks", label: "Title", type: "string", description: undefined, icon: undefined, readonly: false, enum: undefined, minimum: undefined, maximum: undefined, format: undefined, createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedAt: Date.now(), updatedBy: { type: "user", id: "me" }, }, done: { id: "done", collectionId: "tasks", label: "Done", type: "checkbox", description: undefined, icon: undefined, readonly: false, enum: undefined, minimum: undefined, maximum: undefined, format: undefined, createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedAt: Date.now(), updatedBy: { type: "user", id: "me" }, }, }, documents: [], size: 2, createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedAt: Date.now(), updatedBy: { type: "user", id: "me" }, role: "editor", viewId: "view-1", views: [ { id: "view-1", type: "table", collectionId: "tasks", manualSortOrder: ["doc-1", "doc-2"], filters: undefined, sorts: undefined, columns: [ { propertyId: "title", width: 250, visible: true }, { propertyId: "done", width: 100, visible: true }, ], createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedAt: Date.now(), updatedBy: { type: "user", id: "me" }, role: "editor", }, ],});
const documentSource = new InMemoryDocumentSource([ { id: "doc-1", collectionId: "tasks", props: { title: { value: "Buy groceries", ref: undefined }, done: { value: "false", ref: undefined }, }, createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedBy: { type: "user", id: "me" }, updatedAt: Date.now(), role: "editor", meta: undefined, }, { id: "doc-2", collectionId: "tasks", props: { title: { value: "Walk the dog", ref: undefined }, done: { value: "true", ref: undefined }, }, createdBy: { type: "user", id: "me" }, createdAt: Date.now(), updatedBy: { type: "user", id: "me" }, updatedAt: Date.now(), role: "editor", meta: undefined, },]);Then pass them to useCollectionManager:
const state = useCollectionManager({ collectionId: "tasks", user: "me", initialize: async () => ({ collectionSource, documentSource, }),});REST Sources
Section titled “REST Sources”Connect to a REST API. The library provides RestCollectionSource and RestDocumentSource that map to standard HTTP endpoints.
import { RestCollectionSource, RestDocumentSource } from "@blocknote/block-view/core/sources/rest";
const options = { baseUrl: "https://api.example.com",};
const collectionSource = new RestCollectionSource(options);const documentSource = new RestDocumentSource(options);Default Route Mapping
Section titled “Default Route Mapping”| Operation | Method | Default URL |
|---|---|---|
| Fetch collection | GET | {baseUrl}/collections/{collectionId} |
| Update collection | PATCH | {baseUrl}/collections/{collectionId} |
| Fetch documents | GET | {baseUrl}/collections/{collectionId}/documents |
| Create document | POST | {baseUrl}/collections/{collectionId}/documents |
| Update document | PATCH | {baseUrl}/collections/{collectionId}/documents/{documentId} |
| Delete document | DELETE | {baseUrl}/collections/{collectionId}/documents/{documentId} |
Custom Routes
Section titled “Custom Routes”Override any route by providing a routes object:
const options = { baseUrl: "https://api.example.com", routes: { collection: (collectionId) => `/projects/${collectionId}/schema`, documents: (collectionId) => `/projects/${collectionId}/rows`, document: (collectionId, documentId) => `/projects/${collectionId}/rows/${documentId}`, },};Custom Fetch
Section titled “Custom Fetch”Replace the fetch implementation entirely — useful for adding auth headers or using a different HTTP client:
const options = { baseUrl: "https://api.example.com", fetch: async (request) => { const headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${getToken()}`); return fetch(new Request(request, { headers })); },};Broadcast Sources
Section titled “Broadcast Sources”A decorator that wraps any other source to add cross-tab synchronization via the BroadcastChannel API. When a user has multiple tabs open, changes in one tab instantly appear in the others.
import { BroadcastCollectionSource, BroadcastDocumentSource,} from "@blocknote/block-view/core/sources/broadcast";import { RestCollectionSource, RestDocumentSource } from "@blocknote/block-view/core/sources/rest";
const restOptions = { baseUrl: "https://api.example.com" };
// Wrap REST sources with broadcast synchronizationconst collectionSource = new BroadcastCollectionSource(new RestCollectionSource(restOptions));const documentSource = new BroadcastDocumentSource(new RestDocumentSource(restOptions));The broadcast sources are decorators — they delegate all CRUD operations to the wrapped source and add a BroadcastChannel subscription layer on top.
Building a Custom Source
Section titled “Building a Custom Source”Implement the DocumentSource and CollectionSource interfaces to connect to any backend:
import type { CollectionSource, DocumentSource, FetchDocumentsParams, PageResult, DocumentJSON, CollectionJSON,} from "@blocknote/block-view/core/sources";
class MyCollectionSource implements CollectionSource { async fetch(collectionId: string): Promise<CollectionJSON> { const response = await myApi.getCollection(collectionId); return response; }
async update(ctx, changes): Promise<void> { await myApi.updateCollection(ctx.collectionId, changes); }
// Optional: enable real-time updates subscribe(collectionId, applyRemote) { const ws = new WebSocket(`wss://api.example.com/collections/${collectionId}`); ws.onmessage = (event) => applyRemote(JSON.parse(event.data)); return () => ws.close(); }}
class MyDocumentSource implements DocumentSource { async fetch( collectionId: string, params: FetchDocumentsParams, ): Promise<PageResult<DocumentJSON>> { const response = await myApi.getDocuments(collectionId, params); return { data: response.documents, totalCount: response.total, next: response.nextCursor ? { cursor: response.nextCursor } : undefined, }; }
async create(ctx): Promise<void> { await myApi.createDocument(ctx.collectionId, ctx.document.toJSON()); }
async update(ctx, changes): Promise<void> { await myApi.updateDocument(ctx.collectionId, ctx.documentId, changes); }
async delete(ctx): Promise<void> { await myApi.deleteDocument(ctx.collectionId, ctx.documentId); }}The only requirement is that fetch returns data matching the CollectionJSON and DocumentJSON shapes. See the Core API Reference for the full type definitions.