Background Mobile

How to Make an App Like Notion

frontend/
September 17, 2026
How to Make an App Like Notion

Building a productivity tool like Notion is one of those projects that looks deceptively simple until you're three months in and realising your block-based editor needs to handle real-time collaboration, offline sync, and nested data structures simultaneously. This post walks through the actual architecture decisions you'll face, the trade-offs at each layer, and where the genuine complexity lives.

What Makes Notion's Architecture Unusual?

Most SaaS tools store data in predictable relational schemas. Notion doesn't. Its core abstraction is the block, a recursive tree structure where every piece of content, whether a paragraph, a database, a toggle, or an embedded page, is a node. Each block has a type, a set of properties, and a parent reference. This means your schema is essentially a graph, not a set of flat tables.

The practical consequence: you can't just reach for a standard ORM and call it done. You need to think carefully about how you query nested structures at scale. Notion reportedly serves tens of millions of users, and their engineers have written publicly about moving away from a monolithic PostgreSQL setup to a sharded architecture specifically because of how block queries fan out.

The Block Data Model

At minimum, a block record needs:

  • A UUID (not an auto-increment integer — you need globally unique IDs for sync)
  • parent_id and parent_type (blocks can be children of pages or other blocks)
  • type (paragraph, heading, to-do, database, etc.)
  • properties (a JSON column storing type-specific data)
  • content (an ordered list of child block IDs)
  • created_by, last_edited_by, created_time, last_edited_time

The content field storing ordered child IDs is a deliberate design choice. It keeps ordering logic out of the parent-child join and makes reordering O(1) at the record level, at the cost of requiring a second query to resolve the children. That trade-off is fine at low scale and starts hurting at high scale when you're loading deeply nested pages.

PostgreSQL with JSONB handles this well up to a few million blocks. Beyond that, you're looking at either sharding by workspace ID or moving block content to a document store like MongoDB or DynamoDB, with PostgreSQL retained for relational data (users, permissions, billing).

How Do You Build a Block-Based Rich Text Editor?

The editor is where most teams underestimate effort. A plain contenteditable div will get you nowhere near Notion's behaviour. You need a proper editor framework.

ProseMirror is the most mature option. Notion's editor is built on it. It gives you a schema-validated document model, a transaction system for atomic changes, and a plugin architecture. The downside is the learning curve — ProseMirror has a steep API, and building custom block types requires understanding its node and mark system in depth.

Slate.js is the other common choice. It's more approachable, uses React's component model directly, and is easier to customise quickly. It's less battle-tested for complex collaborative scenarios, and you'll likely hit edge cases in its normalisation logic.

Tiptap wraps ProseMirror with a cleaner API and has grown significantly since v2. For most teams building a Notion-like product, Tiptap is the pragmatic starting point. You get ProseMirror's stability without writing raw ProseMirror schemas from scratch.

Whichever you choose, budget time for these specific problems:

  1. Slash commands (the / menu for inserting block types)
  2. Drag-and-drop block reordering with accurate drop targets
  3. Multi-block selection and bulk operations
  4. Handling paste from external sources (Word, Google Docs, raw HTML)

Paste handling alone can take two to three weeks to get right.

/// Not sure where to start?

Get the architecture before you commit

Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.

Real-Time Collaboration: CRDTs vs Operational Transforms

This is the hardest technical decision in the entire project.

Notion uses a combination of server-side reconciliation and their own conflict resolution logic. For a new build, you have two realistic options:

Approach Library Pros Cons
Operational Transform (OT) ShareDB + json0 Mature, well-documented Complex server coordination, hard to reason about
CRDT Yjs, Automerge Peer-to-peer capable, offline-first Higher memory overhead, learning curve
Hosted service Liveblocks, PartyKit Fast to integrate Vendor dependency, cost at scale

Yjs is currently the strongest open-source CRDT library for rich text. It integrates directly with ProseMirror and Tiptap via y-prosemirror. Offline changes merge automatically when the user reconnects. You'll still need a signalling server (a WebSocket server that syncs Yjs document updates between clients), but the conflict resolution logic lives in the library.

The honest trade-off: CRDTs consume more memory than OT because they retain deletion metadata. For very large documents with heavy edit histories, this is measurable. For most productivity tool use cases, it's acceptable.

If you're building an MVP and need collaboration working in under a month, look at Liveblocks. It abstracts the WebSocket infrastructure and gives you a room-based API. You'll pay roughly $0.015 per monthly active user at the Starter tier, which is fine until you're past a few thousand users.

How Should You Structure the Backend and API?

The API layer is less exotic than the editor, but there are choices that affect you later.

Notion's public API uses a RESTful design with block-level granularity. For internal use, you'll want something more efficient for bulk reads. GraphQL works well here because clients can request exactly the block tree they need in one round trip, rather than fetching parent then children then grandchildren separately.

For the backend stack, the combination that works cleanly:

  • Node.js with TypeScript for the API server (fast iteration, good WebSocket support via ws or socket.io)
  • PostgreSQL for structured data, permissions, and workspace metadata
  • Redis for presence (who's currently editing a page), ephemeral collaboration state, and caching frequently accessed pages
  • S3-compatible storage (AWS S3 or Cloudflare R2) for file uploads and image assets

Permissions are non-trivial. Notion's model has workspace-level, page-level, and block-level access control, with inheritance. Implementing this correctly with good query performance usually means a separate permissions table with a materialised view or denormalised cache, not inline checks at query time.

Background Jobs

Several operations should be async:

  • Full-text indexing (Elasticsearch or Typesense for search)
  • Export to PDF or Markdown
  • Sending notifications and emails
  • Thumbnail generation for image and file blocks

Use a proper job queue. BullMQ on Redis is a solid choice for Node.js backends. Don't handle these in your API request lifecycle.

Mobile and Desktop: Where the Complexity Compounds

If you need mobile apps, the editor layer becomes significantly harder. react-native-webview running a web editor is the pragmatic shortcut, and several Notion competitors have shipped it. Native mobile editors using something like react-native-rich-text are possible but require rebuilding the entire block interaction model for touch.

For desktop, Electron with your web app is the fastest path. Notion's desktop app is Electron-based. The trade-off is bundle size (120MB+) and memory usage. If those matter for your users, Tauri is a leaner alternative that uses the system WebView, but the ecosystem is younger.

Offline support on mobile deserves its own planning session. SQLite via expo-sqlite or WatermelonDB can store a local block cache, but syncing that with server state when the user reconnects requires careful conflict resolution logic, especially if you're not using a CRDT approach.

Conclusion

Building a Notion-like product is a genuine multi-discipline engineering project. The block model, the editor, real-time sync, and permissions each carry enough complexity to occupy a small team for months. The technology choices at each layer interact: your CRDT library affects your editor choice, your block schema affects your database architecture, your permissions model affects your API design.

The practical next step: start with Tiptap for the editor, Yjs for collaboration, and PostgreSQL with JSONB for the block store. Get a working single-user editor with persistence before adding any collaboration infrastructure. The temptation to design everything upfront is strong, but the editor behaviour will surface schema requirements you won't anticipate in advance.

If you're scoping this project and want a technical review of your approach before you commit to an architecture, the team at Sodio has built systems in this space and can give you an honest read on where your plan is solid and where it's likely to cause pain later.

FAQ

How long does it take to build a Notion-like app? A functional single-user block editor with persistence takes a small team roughly two to three months. Adding real-time collaboration, mobile support, and a permissions model extends that to nine to twelve months for a production-ready product. Most teams underestimate the editor and sync layers specifically.

What's the biggest technical risk in building a block-based editor? Paste handling and mobile keyboard behaviour are the two most common sources of regression. Rich text paste from external tools (Word, Google Docs, web pages) is inconsistent across browsers and requires extensive normalisation logic. On mobile, virtual keyboard interactions with a web-based editor introduce edge cases that are difficult to reproduce reliably.

Should I use Notion's API instead of building my own? Notion's public API is useful for integrations and automations but is not suitable as a backend for a product. It has rate limits (3 requests per second per integration), read-only access to some block types, and latency that makes it unsuitable for real-time editing. Build your own backend if you're creating a product.

Is PostgreSQL sufficient for a block-based data model at scale? PostgreSQL with JSONB handles block storage comfortably up to tens of millions of blocks if you shard by workspace. Beyond that, you'll need to either horizontally shard PostgreSQL or move block content to a document store. Most early-stage products won't hit this ceiling for several years.

What's the difference between building this with a CRDT versus operational transforms? CRDTs (like Yjs) handle conflicts locally without a central server coordinating the merge. OT (like ShareDB) requires a server to sequence and transform operations. CRDTs are easier to reason about for offline-first scenarios. OT is more established and has a larger body of production case studies, particularly in Google Docs-style tooling.

Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.

/// Work with us

Talk to the engineers who'd build it

You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.

Contact Us