BlockSuite Extensions
BlockSuite is AFFiNE’s powerful editor framework that provides a flexible extension system for blocks, widgets, commands, and services. This guide covers the core extension architecture and how to extend the editor.
Overview
BlockSuite’s extension system is built on dependency injection and allows you to:
Create custom blocks with rich editing capabilities
Extend existing blocks with new functionality
Add widgets and UI components to the editor
Register commands for user interactions
Implement adapters for import/export formats
Extension Architecture
Extension Types
BlockSuite supports two main categories of extensions:
Store Extensions Handle data persistence, schemas, and adapters. Run in non-DOM environments.
View Extensions Manage UI rendering, blocks, widgets, and visual components. Require DOM.
Base Extension Class
All extensions inherit from the base Extension class:
import { Extension } from '@blocksuite/store' ;
import type { Container } from '@blocksuite/global/di' ;
abstract class Extension {
static setup ( di : Container ) : void {
// Register services and implementations
}
}
Extensions use dependency injection to register services, implementations, and factories that can be retrieved by different parts of the application.
See: ~/workspace/source/blocksuite/framework/store/src/extension/extension.ts:126
Store Extensions
Creating a Store Extension
Store extensions extend the StoreExtension class and are used to define block schemas, transformers, and adapters:
import { StoreExtension } from '@blocksuite/store' ;
export class MyBlockStoreExtension extends StoreExtension {
static readonly key = 'my-block' ;
constructor ( readonly store : Store ) {
super ( store );
}
// Called when yjs document is loaded
loaded () {
console . log ( 'Store extension loaded' );
}
// Called when yjs document is disposed
disposed () {
console . log ( 'Store extension disposed' );
}
static override setup ( di : Container ) {
di . add ( this , [ StoreIdentifier ]);
di . addImpl (
StoreExtensionIdentifier ( this . key ),
provider => provider . get ( this )
);
}
}
Important : You must override the key property with a unique string identifier for your extension.
See: ~/workspace/source/blocksuite/framework/store/src/extension/store-extension.ts:19
Store Extension Provider
Use StoreExtensionProvider to register multiple store extensions:
import {
StoreExtensionProvider ,
type StoreExtensionContext
} from '@blocksuite/affine-ext-loader' ;
import { ParagraphBlockSchemaExtension } from '@blocksuite/affine-model' ;
import { ParagraphBlockAdapterExtensions } from './adapters' ;
export class ParagraphStoreExtension extends StoreExtensionProvider {
override name = 'affine-paragraph-block' ;
override setup ( context : StoreExtensionContext ) {
super . setup ( context );
context . register ( ParagraphBlockSchemaExtension );
context . register ( ParagraphBlockAdapterExtensions );
}
}
Store extensions can register schemas and adapters for import/export formats (HTML, Markdown, Plain Text, etc.)
See: ~/workspace/source/blocksuite/affine/blocks/paragraph/src/store.ts:9
View Extensions
Creating a View Extension
View extensions manage the rendering and interaction layer of blocks:
import {
ViewExtensionProvider ,
type ViewExtensionContext
} from '@blocksuite/affine-ext-loader' ;
import { BlockViewExtension , FlavourExtension } from '@blocksuite/std' ;
import { literal } from 'lit/static-html.js' ;
import { z } from 'zod' ;
const optionsSchema = z . object ({
getPlaceholder: z . optional (
z . function (). args ( z . instanceof ( MyBlockModel )). returns ( z . string ())
),
});
export class MyBlockViewExtension extends ViewExtensionProvider <
z . infer < typeof optionsSchema >
> {
override name = 'my-block' ;
override schema = optionsSchema ;
// One-time initialization (runs once per provider class)
override effect () : void {
super . effect ();
// Register lit elements, initialize global state
this . registerLitElements ();
}
override setup (
context : ViewExtensionContext ,
options ?: z . infer < typeof optionsSchema >
) {
super . setup ( context , options );
context . register ([
FlavourExtension ( 'my:block' ),
BlockViewExtension ( 'my:block' , literal `my-block-component` ),
// Additional extensions...
]);
}
}
See: ~/workspace/source/blocksuite/affine/blocks/paragraph/src/view.ts:35
View Scopes
View extensions support different rendering contexts:
Standard Views
Preview Views
Mobile Views
page - Standard page view
edgeless - Edgeless (whiteboard) view
preview-page - Page preview view
preview-edgeless - Edgeless preview view
mobile-page - Mobile page view
mobile-edgeless - Mobile edgeless view
You can conditionally register extensions based on the scope:
override setup ( context : ViewExtensionContext , options ?: Options ) {
super . setup ( context , options );
context . register ([ CommonExtensions ]);
if ( context . scope === 'page' ) {
context . register ([ PageSpecificExtensions ]);
} else if ( context . scope === 'edgeless' ) {
context . register ([ EdgelessSpecificExtensions ]);
}
}
Block Extensions
Block View Extension
Register a Lit template for rendering a block:
import { BlockViewExtension } from '@blocksuite/std' ;
import { literal } from 'lit/static-html.js' ;
const MyListBlockViewExtension = BlockViewExtension (
'affine:list' ,
literal `my-list-block`
);
See: ~/workspace/source/blocksuite/framework/std/src/extension/block-view.ts:24
Flavour Extension
Register a block flavour (type identifier):
import { FlavourExtension } from '@blocksuite/std' ;
const MyFlavourExtension = FlavourExtension ( 'my:flavour' );
See: ~/workspace/source/blocksuite/framework/std/src/extension/flavour.ts:18
Built-in Store Extensions
BlockSuite includes many built-in store extensions:
Block Extensions
Inline Extensions
GFX Extensions
import {
AttachmentStoreExtension ,
BookmarkStoreExtension ,
CodeStoreExtension ,
DatabaseStoreExtension ,
ImageStoreExtension ,
ListStoreExtension ,
ParagraphStoreExtension ,
TableStoreExtension ,
} from '@blocksuite/affine-block-*' ;
See: ~/workspace/source/blocksuite/affine/all/src/extensions/store.ts:34
Built-in View Extensions
Corresponding view extensions for rendering:
Block Views
Widget Views
Fragment Views
import {
AttachmentViewExtension ,
CodeBlockViewExtension ,
DatabaseViewExtension ,
ImageViewExtension ,
ListViewExtension ,
ParagraphViewExtension ,
} from '@blocksuite/affine-block-*' ;
See: ~/workspace/source/blocksuite/affine/all/src/extensions/view.ts:60
Widgets provide UI components that are attached to blocks:
import { WidgetViewExtension } from '@blocksuite/std' ;
import { literal } from 'lit/static-html.js' ;
const MyWidgetViewExtension = WidgetViewExtension (
'my:flavour' ,
'my-widget' ,
literal `my-widget-view`
);
Widget views are rendered alongside their target block’s view. The z-index is determined by the registration order.
See: ~/workspace/source/blocksuite/framework/std/src/extension/widget-view-map.ts:22
Command System
Commands provide a powerful way to execute editor operations:
Creating Commands
import type { Command } from '@blocksuite/std' ;
const myCommand : Command <
{ firstName : string ; lastName : string },
{ fullName : string }
> = ( ctx , next ) => {
const { firstName , lastName } = ctx ;
const fullName = ` ${ firstName } ${ lastName } ` ;
return next ({ fullName });
};
Running Commands
Execute Single Command
Chain Commands
Try Multiple Commands
const [ success , data ] = commandManager . exec ( myCommand , {
firstName: 'John' ,
lastName: 'Doe'
});
const [ success , data ] = commandManager
. chain ()
. pipe ( myCommand1 )
. pipe ( myCommand2 , payload )
. run ();
const [ success , data ] = commandManager
. chain ()
. try ( chain => [
chain . pipe ( command1 ),
chain . pipe ( command2 ),
chain . pipe ( command3 ),
])
. run ();
Command chains stop executing if a command fails. Use try() to attempt multiple commands until one succeeds, or tryAll() to run all commands regardless of success.
See: ~/workspace/source/blocksuite/framework/std/src/command/manager.ts:95
Adapter Extensions
Adapters handle import/export for different formats:
Creating Block Adapters
import type { ExtensionType } from '@blocksuite/store' ;
import {
ParagraphBlockHtmlAdapterExtension ,
ParagraphBlockMarkdownAdapterExtension ,
ParagraphBlockPlainTextAdapterExtension ,
} from './adapters' ;
export const ParagraphBlockAdapterExtensions : ExtensionType [] = [
ParagraphBlockHtmlAdapterExtension ,
ParagraphBlockMarkdownAdapterExtension ,
ParagraphBlockPlainTextAdapterExtension ,
];
See: ~/workspace/source/blocksuite/affine/blocks/paragraph/src/adapters/extension.ts:8
Extension Manager
Manage and configure extensions dynamically:
Store Extension Manager
import { StoreExtensionManager } from '@blocksuite/affine-ext-loader' ;
const manager = new StoreExtensionManager ([ MyStoreProvider ]);
manager . configure ( MyStoreProvider , { cacheSize: 100 });
const extensions = manager . get ( 'store' );
View Extension Manager
import { ViewExtensionManager } from '@blocksuite/affine-ext-loader' ;
const manager = new ViewExtensionManager ([ MyViewProvider ]);
manager . configure ( MyViewProvider , { theme: 'dark' });
// Get extensions for different scopes
const pageExtensions = manager . get ( 'page' );
const edgelessExtensions = manager . get ( 'edgeless' );
Dynamic Configuration
// Set configuration directly
manager . configure ( MyProvider , { enabled: true });
// Update using a function
manager . configure ( MyProvider , prev => {
if ( ! prev ) return prev ;
return { ... prev , enabled: ! prev . enabled };
});
// Remove configuration
manager . configure ( MyProvider , undefined );
See: ~/workspace/source/blocksuite/affine/ext-loader/README.md:34
Lifecycle Hooks
Extensions can hook into the editor lifecycle:
Store Extension Lifecycle
export class MyStoreExtension extends StoreExtension {
loaded () {
// Called when yjs document is loaded
this . initializeData ();
}
disposed () {
// Called when yjs document is disposed
this . cleanup ();
}
}
View Extension Lifecycle
export class MyViewExtension extends ViewExtensionProvider {
override effect () : void {
// One-time initialization (runs once per provider class)
super . effect ();
this . registerLitElements ();
this . initializeGlobalState ();
}
}
Best Practices
Unique Keys Always provide unique key properties for store extensions to avoid conflicts.
Schema Validation Use Zod schemas to validate extension options and ensure type safety.
Scope Awareness Conditionally register extensions based on view scope (page vs edgeless).
Cleanup Always implement lifecycle hooks to properly clean up resources.
Example: Complete Block Extension
Here’s a complete example of creating a custom block:
store.ts
view.ts
component.ts
import {
StoreExtensionProvider ,
type StoreExtensionContext
} from '@blocksuite/affine-ext-loader' ;
import { MyBlockSchemaExtension } from './schema' ;
import { MyBlockAdapterExtensions } from './adapters' ;
export class MyBlockStoreExtension extends StoreExtensionProvider {
override name = 'my-custom-block' ;
override setup ( context : StoreExtensionContext ) {
super . setup ( context );
context . register ( MyBlockSchemaExtension );
context . register ( MyBlockAdapterExtensions );
}
}