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 bytype
. - 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.
Related tables around nodes
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 withch
. - 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.