Local-first · CRDT-native · extensible

A workspace you can build on.

OpenBook is a CRDT document model with a pluggable block system, signed plugins, an MCP server, and a self-hostable store. Everything the app does, your code can do too.

page.json
json
// A page is an array of blocks. Reactive blocks are just
// blocks with a registered renderer — the core knows nothing
// about them. A slider publishes a named value; a chart reads it.
[
  { "id": "h",      "type": "heading", "props": { "level": 2 },
    "text": [{ "t": "Compound growth" }] },

  { "id": "months", "type": "slider",
    "props": { "name": "months", "value": 120, "min": 1, "max": 360 } },

  { "id": "chart",  "type": "kitchart",
    "props": { "kind": "line", "source":
      "{series: [{name:'7%', data: Array.from({length: months}, (_, i) => Math.pow(1.07, i/12))}]}"
    } }
]
…renders to this
Loading the live editor…

That JSON on the left is the document on the right — live. Drag the slider.

How it fits together

Primitives, not a framework

Small, composable pieces: a CRDT doc, a block registry, a scope that recomputes, a signed plugin loader, and a thin store.

CRDT documents (Yjs)

Every doc is a Y.Doc. Blocks are Y.Maps; text is Y.Text. Merges are conflict-free across tabs, peers and offline edits.

Block registry

registerCustomBlock() adds a type + renderer + slash command. The core editor is pure UI over the doc — it knows nothing about your blocks.

Reactive scope

Named inputs publish values; formulas and charts evaluate expressions over that scope and recompute on every change.

Signed plugins

Ship TS source + a manifest, zipped and signed with Ed25519. A sucrase loader registers it at runtime. Signing proves provenance.

MCP server

Drive OpenBook from an AI agent: read, create and edit pages over the Model Context Protocol.

Self-hostable store

A small Postgres-backed page store with a documented HTTP API. Bring your own database.

Extend it

Write a block, ship a plugin

The artifact kit is built on the same public API your plugins use. No private hooks.

sparkline-plugin.tsx
tsx
import {api} from '@book.dev/plugin-sdk';

// A plugin is TS source + an openbook.json manifest, zipped and
// Ed25519-signed. Its entry default-exports activate(api); the
// loader (sucrase + CJS) runs it and registers your blocks at
// runtime — the same registry the built-in kit uses.
export default function activate(a: typeof api) {
  a.blocks.register({
    type: 'sparkline', // registered as "<pluginId>/sparkline"
    render: ({block, editor}) => <Sparkline block={block} editor={editor} />,
    slash: {
      label: 'Sparkline',
      hint: 'A tiny inline chart over a named input',
      keywords: 'spark mini chart trend',
      make: () => ({type: 'sparkline', props: {source: '[1,2,3,5,8]'}}),
    },
  });
}
The SDK

Documents as data, in and out

Build documents in code with a deterministic CRDT seed, then export them to Markdown, HTML or a self-contained interactive deck. Migrate legacy content in too.

build-doc.ts
ts
import {createSeededBlockDoc, blocksToMarkdown} from '@book.dev/ui';

// Build a document programmatically (deterministic CRDT seed),
// then render it to Markdown — or HTML, or a slide deck.
const doc = createSeededBlockDoc([
  {type: 'heading', text: 'Release notes', props: {level: 1}},
  {type: 'todo', text: 'Cut the v2 build'},
]);

const md = blocksToMarkdown(doc);
Run it your way

From a CLI to your own server

Seed sample data, drive it from an agent, or host the store yourself.

CLI

Seed the reactive sample, back up and restore, and script your workspace.

MCP

Point an MCP-capable agent at OpenBook and let it author pages directly.

Postgres store

Run the page store against your own database; the API is documented and small.

Read the code, then bend it

It's all there: the CRDT model, the block registry, the plugin loader, the SDK.