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
typeand 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_reactionsstores 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_interactionsrecords 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
documentstable with a 1-to-1 relation to the owning node. - Lists and filters stay fast because they read from
nodeswithout 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
msand 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.