A generation of pioneers (Doug Engelbart, Ted Nelson, Alan Kay, and many more) saw the computer as tool to augment human problem-solving by giving people power over information.
Today, that information mostly remains siloed across tools. Take cloud-based document editors, where pages are their smallest atomic unit. Information is locked inside of pages and files and foldersāthatās reminiscent of how things were done a century ago.
We built Notion on a framework that allows information to stand on its own, free from any constraint or container, instead putting the power in the hands of the user at a granular level. That framework is built on blocks.
Everything you see in Notion is a block. Text, images, lists, a row in a database, even pages themselvesāthese are all blocks, dynamic units of information that can be transformed into other block types or moved freely within Notion. And when put together, blocks create something much greater than the sum of their parts.
This flexibility is at the heart of Notionās mission. While blocks require our engineering team to apply extreme rigor when structuring information, we wanted an atomic, graph-like data model to provide our users with the ability to customize how their information is moved, organized, and shared.
The block model makes Notion unique, and itās the foundation for how Notion thinks about bringing to life what pioneers thought computing, as a medium, could become.
Block basics
Notion blocks are the singular pieces that represent all units of information inside the Notion editor. The attributes of a block determine how that information is rendered and organized.

Every block has the following attributes:
IDāeach block is uniquely identifiable by its ID. You can see the ID of page blocks at the end of the URL in your browser. We use randomly-generated UUIDs (UUID v4) for IDs in Notion.
Propertiesāa data structure containing custom attributes about a specific block. The most common property is
title, which stores the text content of block types like paragraphs, lists, and of course, the title of a page. More elaborate block types require additional or different properties, like a page block in a database with user-defined properties.Typeāevery block has a type, which defines how a block is displayed, and how the blockās properties are interpreted. Notion supports many types of blocks, most of which you can see in the ānew blockā menu that appears when you press the
+button or in the/menu:

In addition to the attributes that describe the block itself, every block has attributes that define their relationship with other blocks:
Contentāan array (or ordered set) of block IDs representing the content inside this block, like nested bullet items in a bulleted list or the text inside a toggle.
Parentāthe block ID of the blockās parent. The parent block is only used for permissions.
How blocks fit together
Notion blocks can be combined with other blocks to make something much more powerfulālike a roadmap thatās been totally customized to your teamās process, tracking progress and holding all project information in one place. We organize all aspects of blocks to make sure they do the right things and live in the right places, enabling users to connect them and further tailor Notion to solve their problems.
Type and properties
The block type is what specifies how the block is rendered in Notionās UIāand depending on that type, we interpret the blockās properties and content differently. You may be familiar with this if you've used the Turn into function in Notion, which allows you to turn one block type into another.
Changing the type of a block doesnāt change the blockās properties or contentāit only changes the type attribute. The information is just rendered differently, or even ignored if the property isnāt used by that block type.
For example, you can see here that a To-do list block is transformed into several other block types. We also check that To-do list item. The ācheckedā property of the To-do list block is ignored when the block is transformed into Heading and Callout block typesābut by the time we come full circle to turn the block back into a To-do list block, it is still checked.
Decoupling property storage from block type allows for efficient transformation and changes to our rendering logic. But itās also essential for collaboration, because we preserve as much user intention as possible.
Content and the render tree
The flexibility of the block model also allows blocks to be nested inside of other blocks, like text inside a toggle or infinitely nested sub-pages inside of pages. The content attribute of a block is what stores the array of block IDs (or pointers) referencing those nested blocks.

In the to-do list example, we have a To-do list block (āWrite a blog post about blocksā) with three block IDs in its content array. We think of these IDs as ādownward pointers,ā and call the blocks that they refer to ācontentā or ārender children.ā

Each block defines the position and order in which its content blocks are rendered. We call this hierarchical relationship between blocks and their render children the ārender tree.ā But, it doesn't look like a tree with branchesādifferent block types render their children in different ways.
Here are a few examples of how the content attribute is rendered by different block types:
List blocksā
Text,Bulleted list, andTo-do list. List blocks display their content indented.Togglesā
Toggle listblocks only display content when expanded. Otherwise, they only display the title property.Pagesā
Pageblocks display their content in a new page, instead of rendering it indented in the current page. To see this content, you would need to click into the new page.
Structurally, this continues to bring power to your information by allowing you to manipulate blocks at the most granular level. Itās a concept that preserves user intent of how information should be organized and displayed when working with other information.
Editing the render tree
Have you ever been surprised by how indentation works in Notion? In conventional word processors, indentation is presentational: it only affects the spacing of text from the margins. In Notion, indentation is structural: itās a reflection of the structure of the render tree. In other words, when you indent something in Notion, you are manipulating relationships between blocks and their content, not just adding a style.
For example, pressing indent in a content block tries to add that block to the content of the nearest sibling block in the content tree.

Most of the time, indenting works like it would in a traditional document editorāthe currently-selected block will move into the content array of the preceding block, and will be rendered indented within. However, when the preceding block isnāt a list (or thereās no previous block at all), indenting will have no effect because there is nowhere to move the block into. The visual representation of a document in Notion reflects the structure of the information it contains.
Permissions
So far, weāve explained how blocks come together to organize and structure your information. Itās also important to understand how this structure protects your information so only the right people can read it or change it.
Blocks inherit permissions based on the blocks in which theyāre located (which are above them in the tree). Consider a pageāto read its contents, you must be able to read the blocks within that page. However, there are two reasons we canāt use the content array to build this permissions system:
Initially, we allowed blocks to be referenced by multiple content arrays to simplify our collaboration and concurrency model. But because a block can be referenced in multiple places, itās ambiguous which block it would inherit permissions from. And ambiguity is unacceptable in a permissions system.
The second reason is mechanical. To implement permission checks for a block, we need to look up the tree, getting that blockās ancestors all the way up to the root of the tree (which is the workspace). Trying to find this ancestor path by searching through all blocksā content arrays is inefficient, especially on the client.

Instead, we use an āupward pointerāāthe parent attributeāfor the permission system. The upward parent pointers, and the downward content pointers mirror each other (outside of a few edge cases weāre working to clean up).

Life of a block
A blockās life starts on the client.
When you take an action in the UIātyping in the editor, dragging blocks around a pageāthese changes are expressed as operations that create or update a single record. Our team refers to ārecordsā as any kind of persisted data in Notion, like blocks, users, workspaces, etc. And because many actions usually change more than one record, operations are batched into transactions that are committed (or rejected) by the server as a group.
Letās say you're working inside a page with a friend, both on separate computers editing a to-do list. What happens behind the scenes?
Creating and updating blocks
You press enterāthis creates a new To-do block.
First, the client defines all the initial attributes of the block, generating a new unique ID, setting the appropriate block type (to_do), and filling in the blockās properties (an empty title, and checked: [["No"]]). It builds operations to represent the creation of a new block with those attributes.
New blocks are not created in isolation: blocks are also added to their parentās content array, so theyāre in the right position in the content tree. So, the client also generates an operation to do so. All these individual change operations are grouped into a transaction.
Then, the client applies the operations in the transaction to its local state. New block objects are created in-memory and existing blocks are modified. On our native apps, we cache all records you access locally in an LRU (least recently used) cache on top of SQLite or IndexedDB called RecordCache. When you change records on a native app, we also update the local copies in RecordCache. The editor re-renders to draw the newly created block onto your screen. This happens within a few milliseconds of your keypress.
At the same time, the transaction is saved into TransactionQueue, the part of the client responsible for sending all transactions to Notionās servers so your data is persisted and shared with collaborators. TransactionQueue stores transactions safely in IndexedDB or SQLite (depending on platform) until theyāre persisted by the server or rejected.
Saving changes on the server
Hereās how your block ends up saved safely on the server, so your friend can see it.
Usually, TransactionQueue sits empty, so the transaction to create the block is sent to Notionās server right away in an API request. The transaction data is serialized to JSON and posted to the /saveTransactions API endpoint.
SaveTransactionās main job is to get your data into our source-of-truth databases, which store all block data, as well as all other kinds of persisted records in Notion.
Once the request reaches the Notion API server:
We load all the blocks and parents involved in the transaction. This gives us a ābeforeā picture in memory. For this example, remember that weāre creating a block. So we need to, at least, load the page block so that we can insert the newly created blockās ID into the pageās content array.
We duplicate the ābeforeā data that had just been loaded in memory. Then, we apply the operations in the transaction to the new copy to create the āafterā data.
Then we use both ābeforeā and āafterā data to validate the changes for permissions and data coherency. If everything checks out (it usually does), all created or changed records are committed to the databaseā meaning your block has now officially been created.
At this point, thereās a āsuccessā HTTP response to the original API request that had been sent by your client. This confirms your client knows the transaction was saved successfully and that it can move onto saving the next transaction in the TransactionQueue.
In the background, we schedule additional work depending on the kind of change made for your transaction. For example, we schedule version history snapshots and indexing block text for Quick Find. Importantly, we also notify MessageStoreāNotion's real-time updates serviceāabout the changes you made.
Weāll cover how the data gets to your friendās screen in the next section.
Real-time updates
You pressed enter, created a new block, and now your block shows up on your friendās screen. How does that work?
Every client has a long-lived WebSocket connection to MessageStore, Notionās real-time updates service. When the Notion client renders a block (or page, or any other kind of record), the client subscribes to changes of that record from MessageStore using this WebSocket connection. When your friend opens the same page as you, theyāre subscribed to changes of all those blocks.
After your changes made it through the saveTransactions process, the API notified MessageStore of new recorded versions. MessageStore finds client connections subscribed to those changing records, and passes on the new version to them through their WebSocket connection.
When your friendās client receives version update notifications from MessageStore, it verifies that version of the block in its local cache. Because the versions from the notification and the local block are different, it sends a syncRecordValues API request to the sever with the list of outdated client records. The server responds with the new record data. The client uses this response data to update the local cache with the new version of the records, then re-renders the UI to display the latest block data.
Reading blocks
Your friend takes a nap, but you continue working on the to-do list. To let them know youāve made some changes to the list, you send them a link to the Notion page youāve both been working in.
In the first few milliseconds after your friend wakes up and clicks the link, we first try to load that page using only local data. On web, this means block data thatās in-memory. On our native apps, we try loading blocks that arenāt in memory from the RecordCache persisted storage. But if we need block data thatās missing, we stop and request the page data from the API instead.
The API method for loading the data for a page is called loadPageChunkāit descends from a starting point (likely the block ID of a page block) down the content tree, and returns the blocks in the content tree plus any dependent records needed to properly render those blocks. We use several layers of caching for loadPageChunk, but in the worst case, this API might need many trips to the database as it recursively crawls down the tree to chase down blocks and their record dependencies.
All data loaded by loadPageChunk is put into memory (and saved in the RecordCache if youāre using the app). Once the data is in memory, we lay out the page and render it using React.
Building blocks for whatās next
Blocks are the most foundational component of Notionās mission to empower any person or business to tailor software to their problems. This architecture sets a path for Notionās futureānew block types, automations (like the API), workflows, and functionality that enables you to create even more powerful tools.
Still, we know thereās a lot of room to grow Notion the product, improving things like efficiency, performance, offline, and block-specific quirks in the editor. What you see when youāre using Notion is only the part of the iceberg thatās above the surface. After reading this, we hope you understand a bit of whatās underneath.
Weāre looking for help to build the future of Notion. Is that you?

