Everything is a block: Blok's data model
Blok has one core idea. Understand it, and the rest of the API falls into place.
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.
// 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.
// 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 paragraph | Its data holds the text and inline formatting. |
| A database | Its data holds the schema and view configs — not a side table. |
| A database row | It's a child of the database block; its values live in data.properties. |
| A page | Any 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.