Skip to content
FrameworkJavaScript

Everything is a block: Blok's data model

Blok has one core idea. Understand it, and the rest of the API falls into place.

Last updated Jun 30, 2026Edit this page on GitHub

In Blok, every piece of content is a block. There is no separate document model running alongside them — blocks are the universal primitive. A paragraph is a block. A heading is a block. A whole database, and every row inside it, is a block. Once this clicks, the API stops reading like a list of features and starts reading like one consistent tree.

The shape of a block

Every block — whatever its type — is the same small object. type names the tool that renders it, data holds that tool's content, and tunes holds optional block-level settings.

TypeScript
// Every block — whatever its type — is the same small object
{
  id: 'block-a1b2c3',              // unique, auto-generated
  type: 'paragraph',              // which tool renders it
  data: { text: 'Hello, world' }, // the tool's content
  tunes: {},                      // block-level settings (optional)
}

Blocks form a tree

Blocks aren't a flat list. Any block can contain other blocks through contentIds, and each child points back through parentId. A block that holds children is, in effect, a page — opening it means navigating into its children.

TypeScript
// A database block holds its rows as children.
// In saved JSON the hierarchy fields are named `content` and `parent`;
// on live Block objects (and the React BlockNode) the same links are
// exposed as `contentIds` and `parentId`.
{
  id: 'db-1',
  type: 'database',
  data: { schema: [/* columns */], views: [/* configs */] },
  content: ['row-1', 'row-2'], // its children
}

// A row is a block that points back to its parent
{
  id: 'row-1',
  type: 'database-row',
  parent: 'db-1',
  data: { properties: { status: 'Done', priority: 'High' } },
}

The same idea, all the way up

Features that look like separate systems are really just blocks arranged in the tree:

This……is a block because
A paragraphIts data holds the text and inline formatting.
A databaseIts data holds the schema and view configs — not a side table.
A database rowIt's a child of the database block; its values live in data.properties.
A pageAny block with children. Opening it navigates into its contentIds.

What is not a block

Structured property values — a status, a priority, a due date on a row — are not blocks. They're metadata stored in the owning block's data. The rule of thumb: if it has an identity, stores content, and can be nested, it's a block; if it's a typed value that belongs to a block, it lives in that block's data.

Why this matters to you

Because everything is a block, you learn one set of moves and they work everywhere:

One API for everything

insert, move, and delete operate on any block — a paragraph, a row, or a whole database — with the same calls.

One save format

editor.save() walks the same tree for every kind of content, so persistence and rendering never need special cases.

One nesting model

parentId and contentIds describe containment everywhere, so a row inside a database and a paragraph inside a toggle behave the same way.

Extending Blok? Ask one question

Before designing a new feature, ask: “Is this a block?” If it represents content, data, or a container, the answer is almost always yes — build it as a block instead of inventing a parallel data model.

  • A calendar view → a view config on a database block; the rows stay row blocks.
  • A comment thread → a block.
  • A table of contents → a block that reads its sibling blocks.
  • An embed → a block.

Ready to work with the tree directly? See the Blocks API for inserting and moving blocks, and BlockData for the exact shape of a saved block.