Skip to content

Codex: Architecture Decision Record

Codex Editor Architecture

Status: DRAFT Issue: ORI-77 Date: 2026-01-09

Summary

This document evaluates block-based editor architectures for Codex, a Notion-like collaborative workspace built on the Orix platform.


Research Findings

1. ProseMirror

Architecture: Schema-based document model with nodes and marks

Collaboration: Operational Transform (OT) via prosemirror-collab plugin

  • Central authority validates and broadcasts “steps”
  • Version tracking for conflict detection
  • Requires server-side coordination

Pros:

  • Mature, well-documented
  • Highly customizable schema
  • Large ecosystem

Cons:

  • Collaboration is an “addon”, not native
  • OT requires central authority (not offline-first)
  • Complex for block-based UIs

2. Lexical (Meta)

Architecture: Node-based (ElementNode, TextNode, DecoratorNode)

Collaboration: Via Yjs binding (@lexical/yjs)

Pros:

  • Modern, actively maintained
  • React-first with good DX
  • Extensible node system
  • JSON serialization built-in

Cons:

  • Less mature than ProseMirror
  • Collaboration requires external CRDT library

3. Yjs (CRDT Library)

Architecture: True CRDT with shared types (Y.Doc, Y.Text, Y.Map, Y.Array)

Key Features:

  • Conflict-free automatic merging
  • Offline-first by design
  • No central authority needed
  • Quill Delta format for rich text

Shared Types:

Y.Text // Rich text with formatting
Y.Map // Key-value store
Y.Array // Ordered list
Y.XmlFragment // Tree structure

Pros:

  • True offline-first
  • Battle-tested (used by many editors)
  • Language-agnostic protocol

Cons:

  • JavaScript only (needs port for C#)
  • Storage format not directly compatible with Axion

4. BlockSuite (AFFiNE)

Architecture: Document-centric, CRDT-native

Key Innovation: CRDT IS the data layer, not an addon

Traditional: Editor State → Sync Layer → Server
BlockSuite: Y.Doc (CRDT) → All Editors Read From It

Block Model:

  • Discrete contenteditable blocks
  • Each block connects to Y.Text node
  • Tree structure via Y.XmlFragment

Pros:

  • Purpose-built for Notion-like editing
  • CRDT-native (Yjs built-in)
  • Block-based from the ground up
  • Web components (framework-agnostic)

Cons:

  • Tightly coupled to Yjs
  • Less flexible than lower-level options

Key Architecture Questions

Q1: How do blocks map to CRDTs?

Answer: Each block is a node in a Y.XmlFragment tree. Block content (rich text) maps to Y.Text. Block metadata maps to Y.Map.

Y.Doc
└── Y.XmlFragment (page)
├── Y.XmlElement (paragraph block)
│ ├── Y.Text (content)
│ └── Y.Map (metadata)
├── Y.XmlElement (heading block)
│ └── ...
└── Y.XmlElement (list block)
└── ...

Q2: Schema for nested blocks?

Answer: Tree structure using parent references or nested Y.XmlFragment:

interface Block {
id: string;
type: BlockType;
parentId: string | null;
children: string[]; // Block IDs
content: Y.Text;
props: Y.Map;
}

Q3: Cursor positions in collaborative editing?

Answer: Awareness protocol (Yjs) or custom presence system:

  • Each client broadcasts cursor position
  • Positions stored as relative CRDT positions (survive edits)
  • Render remote cursors with user colors

Q4: Performance with large documents?

Answer:

  • Lazy loading: Only load visible blocks
  • Virtualization: Render only viewport
  • Chunked sync: Don’t send entire document
  • Yjs handles 10K+ operations efficiently

Recommendation

Strategic Decision: Build vs Buy

ApproachProsCons
A: Use BlockSuite/YjsFast MVP, proven techNot dogfooding Orix
B: Custom CRDT on OrixFull dogfooding, controlMore work, risk
C: HybridBest of bothIntegration complexity

UPDATE: With the discovery of existing Lattice.CRDT infrastructure, we can dogfood Orix from day 1.

Phase 1 (Foundation): Use Lattice.CRDT + Lexical for frontend

  • Leverage existing Rga<T> for text sequences
  • Use OrMap<K,V> and NestedOrMap<K,V> for blocks
  • Build CrdtRichText adapter for Lexical integration
  • Sync via Nexus WebSocket

Phase 2 (Persistence): Full Lattice integration

  • Store in LatticeDB with Chronicle history
  • Implement CrdtTree for block hierarchy
  • Add tombstone garbage collection

Phase 3 (Differentiation): Orix-native features

  • Deterministic replay of editing sessions
  • Time-travel debugging for documents
  • Cross-document CRDT references

Proposed Architecture

Data Layer (Orix)

┌─────────────────────────────────────────┐
│ Codex.Storage │
├─────────────────────────────────────────┤
│ Orix.Lattice.Crdt │
│ ├── CrdtText (rich text) │
│ ├── CrdtMap (block props) │
│ └── CrdtTree (block hierarchy) │
├─────────────────────────────────────────┤
│ Orix.Lattice.Chronicle │
│ └── Version history, snapshots │
├─────────────────────────────────────────┤
│ Orix.Axion │
│ └── codex.axion schema definitions │
└─────────────────────────────────────────┘

Sync Layer (Orix)

┌─────────────────────────────────────────┐
│ Codex.Sync │
├─────────────────────────────────────────┤
│ Orix.Nexus │
│ ├── WebSocket connections │
│ ├── CRDT operation broadcast │
│ └── Presence/awareness │
└─────────────────────────────────────────┘

Frontend

┌─────────────────────────────────────────┐
│ codex-web │
├─────────────────────────────────────────┤
│ React + Lexical │
│ ├── Block components │
│ ├── Slash command menu │
│ └── Collaborative cursors │
├─────────────────────────────────────────┤
│ @orix/crdt-client (TypeScript) │
│ ├── CrdtRichText ↔ Lexical binding │
│ └── WebSocket sync via Nexus │
└─────────────────────────────────────────┘

Axion Schema (Draft)

namespace Codex.Schema;
// Block types
enum BlockType {
Paragraph = 0;
Heading1 = 1;
Heading2 = 2;
Heading3 = 3;
BulletList = 4;
NumberedList = 5;
TodoItem = 6;
CodeBlock = 7;
Quote = 8;
Divider = 9;
Image = 10;
Table = 11;
}
// Document
record Document {
id: Entity;
title: string;
icon: string?;
cover: string?;
parentId: Entity?;
rootBlockId: Entity;
createdAt: Tick;
updatedAt: Tick;
}
// Block
record Block {
id: Entity;
documentId: Entity;
type: BlockType;
parentId: Entity?;
order: DFixed64; // Fractional indexing for reordering
props: map<string, string>;
// Content stored separately as CRDT
}

Blocking Dependencies

Before Codex implementation can proceed:

BlockerIssueStatus
CRDT design for LatticeORI-52COMPLETE - Lattice.CRDT exists
Lattice alpha deploymentORI-49Backlog
Nexus sync implementation(new)Not created

Existing CRDT Infrastructure (Lattice.CRDT)

Discovery: The Orix codebase already contains a comprehensive CRDT library:

ComponentClassCodex Use
HybridLogicalClockHlcTimestampCausality tracking
Sequence CRDTRga<T>Text content
Add-wins SetOrSet<T>Block collections
OR-MapOrMap<K,V>Block properties
Nested OR-MapNestedOrMap<K,V>Block hierarchy
LWW RegisterLwwRegister<T>Simple fields
MV RegisterMvRegister<T>Concurrent edits
Merge EngineMergeEngineEntity merging

Remaining for Codex:

  1. CrdtRichText - Rich text with marks (extends Rga<T>)
  2. CrdtTree - Block hierarchy (using NestedOrMap + fractional indexing)

Next Steps

  1. ORI-78: Finalize Axion schema based on this architecture
  2. ORI-52: Design CRDT strategy for LatticeDONE - Lattice.CRDT exists!
  3. ORI-79: Implement CrdtRichText extending Rga<T> with formatting marks
  4. ORI-82: Start frontend with Lexical + @orix/crdt-client (can parallel)

References