logo
Data Model
Data Model

Data Model

Being an all-in-one workspace, Colanode’s data model is a critical design decision. Users can create many kinds of things-messages, channels, chats, folders, files, databases, views, records, and more over time. Every one of these is part of a workspace, and-because Colanode is local-first-everything a user can access must also be synced to their device for offline use.

Everything a user creates is a node with a custom type and a set of attributes. Nodes form a hierarchy (each node can have a parentId and any number of children). This structure is inspired by the ProseMirror document model-the editor Colanode uses-because it maps well to rich, nested content while staying simple to reason about and sync.

Node shape

At a high level, a node has:

  • a unique id (ULID),
  • a type (e.g., message, channel, page, record),
  • optional hierarchy via parentId,
  • and any number of type-specific attributes.

Example: Channel node

{
  "id": "01jtjq47wrnvpv88hxtg3np70nch",
  "type": "channel",
  "name": "Announcements",
  "avatar": "01jhzbzspgp8r6tj9k3733bw24es",
  "parentId": "01jtjq47vv26rxwygem3mnw429sp"
}

Example: Message node (rich content)

Some node types (like messages and pages) carry rich, nested content following a ProseMirror-style model:

{
  "id": "01jtjq47wze3811r4vaw2etycyms",
  "type": "message",
  "content": {
    "01jtjq47wze3811r4vaw2etyczbl": {
      "id": "01jtjq47wze3811r4vaw2etyczbl",
      "type": "paragraph",
      "index": "a0",
      "content": [
        {
          "text": "Termes aetas adstringo soleo vae aestas vulnero.",
          "type": "text"
        }
      ],
      "parentId": "01jtjq47wze3811r4vaw2etycyms"
    }
  },
  "subtype": "standard",
  "parentId": "01jtjq47wrnvpv88hxtg3np70nch"
}

Why nodes?

A single nodes table (synced locally) keeps the model uniform and the sync engine straightforward. It also plays nicely with TypeScript and runtime validation:

  • TypeScript unions by type. Each node variant becomes a discriminated union keyed by type.
  • Zod schemas per node type. Strong parsing and validation with clear, type-safe attributes.
  • Simpler sync. One primary table to replicate and reconcile.
  • Easy extensibility. To add a new kind of content, define a new node type and its attributes-most of the work is then just UI.

Beyond the nodes table, features that cut across node types live in auxiliary tables:

  • node_reactions stores emoji (or other) reactions per node per user. Today it powers message reactions; in the future it can naturally extend to comments, posts, or any other node.
  • node_interactions records when a user first saw/opened a node and other interaction events. This enables read indicators, “seen by” states, and similar UX across messages and pages.

Large content and the documents table

Some nodes-like pages or database records-can hold large bodies of text. While the core data model and sync engine support this directly inside a node, it can hurt performance for common queries (listing, filtering) that don’t need the full document content. To optimize for this:

  • Large, rarely-listed content is split out into a documents table with a 1-to-1 relation to the owning node.
  • Lists and filters stay fast because they read from nodes without pulling the entire document.
  • When the user opens a page (a get-by-id use-case), the document content is fetched on demand.

ID strategy and other optimizations

Colanode uses ULIDs for all entity IDs. This has two important benefits:

  • Client-side generation. Devices can create globally unique IDs offline.
  • Lexicographic order by time. ULIDs sort by creation time, so ordered listings (e.g., latest messages) can simply ORDER BY id, often avoiding extra indexes.

Inspired by Stripe’s readable IDs, Colanode also appends a short postfix that encodes the entity type. We use a postfix (not a prefix) so the time-ordering property of ULIDs remains intact:

  • For example, message IDs end with ms and channel IDs with ch.
  • This makes it easy to infer the entity kind from the ID alone (handy in UI routing/rendering without fetching the full record).

In short, the node model keeps Colanode consistent, extensible, and efficient to sync. Hierarchy models real-world relationships; per-type attributes capture specifics without fracturing the storage layer; and the documents split plus ULID strategy keeps everyday operations fast—online or off. The node-based data model is also the inspiration behind the name Colanode: collaboration + node.