SIM 3.0.4: Content Management On-Chain with Motoko
- written by Roland BOLE (Instructor)
This article provides an update on the SIM 3.0 project and briefly explores how to implement content management on the Internet Computer.
Blog image

Current Project Status

After the completion of file uploading, downloading, and on-chain storage in distinct data canisters, we reached the next project milestone and got a new login screen for the SIM 3.0.4 version.

SIM 3.0.4 Integrating content management

The content management for projects is structured around the following key components:

  • A document tree featuring an unlimited number of hierarchical levels;
  • Each node within the tree capable of containing multiple content elements.

Currently, the implemented content elements include:

  • Text: We have chosen the Tiptap editor to handle all writing operations. The editor allows linking to both external resources and, more significantly, to internal nodes within the project.
  • Images: Images in this project are not managed differently from other file types. Instead of being stored as base64 strings, image data is stored as regular files and cached if watched on the frontend to enhance user experience and mitigate minor latency issues associated with the Internet Computer.
  • Files: An content element that allows attaching multiple files to a content node.

A short video showcases the process of using the content management section of the application.

star icon

How this Kind of Data is Stored

We utilize the OrderedMap data structure from the Motoko language to store this type of data. The decision was driven by its suitability for managing both the document tree (nodes and their children) and the various content elements. Specifically, when a node is selected, all associated content elements are loaded in a subsequent step, making the OrderedMap an efficient solution for this two-stage retrieval process.

Highlight img
OrderedMap

In Motoko, the OrderedMap is a stable key-value data structure implemented as a red-black tree, ensuring that keys are maintained in a sorted order. This structure allows efficient operations such as insertion, deletion, and lookup, all with a worst-case time complexity of O(log2n). The separation of data and operations into distinct types enhances stability, making OrderedMap particularly suitable for canister smart contracts that require persistent and upgrade-friendly storage solutions.

Beyond its performance benefits, OrderedMap offers a rich functional API inspired by the OCaml standard library. This includes methods like put, replace, entries, and minEntry, among others, facilitating expressive and concise data manipulations. Its design not only improves previous implementations like RBTree by offering better type stability and a more comprehensive API, but also aligns with functional programming paradigms, promoting cleaner and more maintainable code. Read more about OrderedMap

O(log2n) (read as “order of log n”) is a way of expressing how the time or effort needed to perform an operation grows as the size of the input (n) increases.

If something takes O(log2n) time, it means:

  • As your dataset grows larger, the time it takes to perform an operation (like lookup, insert, or delete) increases slowly.
  • Specifically, it grows proportionally to the logarithm of the input size, not linearly.

Let’s say you’re using an OrderedMap with these many items:

publicNumber of Entries (n)bricklog2(n) = Steps needed
public8brick3
public16brick4
public1,000brick10
public1,000,000brick20

So even with 1 million items, it would only take about 20 steps to find or insert something, thanks to the internal balanced binary tree.

To wrap up: O(log2n) means that the algorithm is very efficient, even with large data sets. That’s why data structures like OrderedMap in Motoko (which uses red-black trees) are fast and scalable.

For docuProject, we’ve established a datastore to manage a distinct document node tree for each project. Additionally, a secondary datastore, docuProjectContent, is used to house all content elements across the projects.

Below you can see the structure for the docuProject store.

DocuProject Structure

Content elements are stored using an OrderedMap, leveraging its benefits, but with a specific desired behavior.

Defining an OrderedMap in Motoko requires specifying a type due to its strict type system. A potential issue arises when a datastore contains diverse datatypes. Yet, only a single type can be defined for the OrderedMap structure. Currently, there are three distinct types, each possessing specific desired properties as illustrated in the image below.

Traditional web development, such as SQL or NoSQL databases, presents different data structuring approaches, too. In SQL, all attributes are typically combined into a single large table. Conversely, NoSQL JSON databases allow more flexible structures, where each entity (like editor, image, or file) can be represented in separate documents, each containing its specific properties.

Motoko can achieve flexibility through the VARIANT type. Representing diverse content blocks - like editor text, images, and files - a variant offers a type-safe and structured way to manage rich, varied data. This approach, unlike loosely typed records or optional fields, clearly labels each content block and includes only its relevant data. Consequently, this eliminates ambiguity and simplifies the logic for rendering, storing, or transforming the content.

The key advantage of this pattern is its extensibility and robustness. You can easily introduce new content types by adding new variants, without affecting existing logic. Combined with Motoko’s powerful pattern matching, this makes variant types ideal for building dynamic and modular document models or UI structures - ensuring a safe, maintainable code that adapts as your application evolves.

The store schema for this scenario is provided below.

DocuProjectContent Structure

The definition for this specific type is provided below.

public type DocuProjectDocument = {
  #Editor : {
    content: Text;

    projectId: Nat;
    nodeId: Nat;
    created: Int;
    createdBy: Nat;
    lastUpdate: Int;
    lastUpdateBy: Nat;
  };
  
  #Image : {
    fileId: Text;
    projectId: Nat;
    nodeId: Nat;
    created: Int;
    createdBy: Nat;
    fileBelongsToCanister: Principal;
  };

  #File : {
    files:[DocuFile];
    projectId: Nat;
    nodeId: Nat;
    created: Int;
    createdBy: Nat;
  };
};

Stay tuned for more updates and feel free to reach out if you have any questions.