Skip to main content

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:
  • page - Standard page view
  • edgeless - Edgeless (whiteboard) 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:
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:
import {
  AttachmentViewExtension,
  CodeBlockViewExtension,
  DatabaseViewExtension,
  ImageViewExtension,
  ListViewExtension,
  ParagraphViewExtension,
} from '@blocksuite/affine-block-*';
See: ~/workspace/source/blocksuite/affine/all/src/extensions/view.ts:60

Widget Extensions

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

const [success, data] = commandManager.exec(myCommand, {
  firstName: 'John',
  lastName: 'Doe'
});
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:
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);
  }
}