Quick Start
CoCube combines the reactivity of spreadsheets, the workflow of visual design software, and the utility of a local SQLite database into a single system. With it, you can create, share, and collaborate on vector-rendered tools - from simple interactive diagrams to complex documents. This guide covers the core concepts you need to get started.
A desktop browser with WebGPU support is required (Google Chrome is highly recommended). Mobile browsers are beginning to support WebGPU, but expect some instability.
Please join the Discord 🛸 to ask questions, share feedback, and report issues! Your input will directly shape the product.
What is CoCube?
A private, local, and collaborative database
Think of CoCube as a private database with an interface that you create. The component system allows you to build nearly any tool you need for working with your data. Workspaces allow you to link components to your database. Unlike most knowledge management tools, flexibility is the point. CoCube only provides you with a system for storing, viewing, and editing data. It is up to you to determine the best way to use it.
Keeping your work private, local, and accessible is a core pillar of CoCubes design. All of your data is stored only on your local device. When you're ready to collaborate, you can selectively sync specific data to CoCube's servers. Components referencing that data instantly become collaborative - no rewiring required. And because the system is built on conflict-free replicated data types (CRDTs), your tools keep working even when you're offline, merging changes seamlessly when you reconnect.
A more flexible spreadsheet
CoCube's reactive model builds on the same core idea as spreadsheets: cells hold values or expressions, and when one cell changes, everything that depends on it updates automatically. The difference is scope.
In a traditional spreadsheet, cells live in a grid. In CoCube, cells define every attribute of a visual component: its width, height, color, shape, layout, and behavior. Change a cell that defines a component's color, and the component updates instantly. Write an expression that computes a layout from data, and the layout recalculates when the data changes. Wire up an event handler, and the component becomes interactive.
Components can be nested, positioned freely, and arranged in dynamic layouts. You can build anything from a diagram to a fully interactive dashboard, all using the same cell and expression system.
An alternative to writing custom software
CoCube uses a declarative, expression-based language for building components. It's much simpler than HTML, CSS, and JavaScript, but it is designed to be flexible where it matters. Many similar tools give you opaque, pre-built components with limited customization. CoCube takes a different approach: every component it ships with uses the same reactive cell language you have access to. Even the document explorer and component editor themselves were built in this system. If something doesn't work the way you want, you can modify it or build your own from scratch.
CoCube renders everything through a GPU-accelerated vector graphics engine. Components aren't HTML elements — they're drawn directly on a GPU canvas, so rendering stays fast regardless of complexity.
To get you started, CoCube includes a set of built-in components - enough to build anything from to-do lists to note-taking systems to custom data visualizations.
How CoCube works
The following sections cover the three core concepts you need to understand to get started using CoCube:
- Datums: The data you create in your database.
- Components: Visual tools used to create, display, and modify datums.
- Workspaces: Where you attach components to your database to create tools.
What is a Datum?
Datums are the fundamental unit of storage in CoCube. Each datum holds a single value, such as text, a number, or a color. Every datum has a unique identifier and can be both referenced in expressions and edited by components.
Datums are reactive. When you interact with a component — for example, typing into a text editor, or clicking a button — that component is likely modifying a datum.
The moment the datum changes, CoCube's reactivity system identifies every cell expression that depends on it and re-evaluates it. Any change to a datum, whether from your interaction, a collaborator's edit, or a sync from another device, follows this process. This bidirectional flow between components and datums is what makes CoCube capable of building interactive tools.
For a complete explanation of how datums are stored, synced, and shared, see the Data Storage page.
What is a Component?
Components are composable visual elements used to modify datums. You can combine, customize, and create them entirely within CoCube. Similar to a spreadsheet, all attributes of a component are defined by reactive cells. Unlike a spreadsheet, a component isn't displayed as a grid — its cells define how it looks and behaves.
Here is a very simple component.
| Height | 100 |
| Width | 100 |
This component appears as a non-interactive green square (default color, default shape) with a width and height of 100. Width and Height are the names of its two cells. Any cell can hold a plain value or a spreadsheet-like expression, making it easy to add dynamic behavior.
Cells
Every cell has a unique name and contains an expression that computes its value. You can name cells anything you like, but no two cells in the same component can share a name. In this guide, inline code that looks like Height or Width refers to a cell name.
Some cells have special names that control appearance and behavior — these are called attribute cells and are covered in an upcoming section.
Cells can be defined using complex expressions. For example, the Width cell expects a Number. Instead of providing a number directly, you could set it to an expression that returns a number:
In this guide, text formatted like Number, Color, or Bool indicates a data type. Most are self-explanatory: Number is a number, Text is a string like "hello", a Bool can be true or false. An Array is a list of values:
A Map is a set of named values:
| "Item" | "Bubblegum" |
| "Cost" | 3 |
Additional types like Color, EventHandler, and Anchor are covered later.
What is a Workspace?
A workspace is where you connect your components to your database. When you use CoCube, you will most often be working from a workspace.
A component, on its own, defines a visual tool that displays and manipulates some datum. In a workspace, that component is connected to one or more specific datums in your local database.
Building with workspaces
A simple workspace might contain a single text editor. Typing into it persistently updates a text datum in your local database. Share that workspace, and both you and a collaborator can edit the same text — giving you a collaborative document editor with no extra setup.
A more complex workspace might combine a data table, a chart component, and a set of filter controls. Each reads from and writes to datums in your database. Changing a filter updates the table, which updates the chart — all reactively, with no code beyond cell expressions. Workspaces let you compose components into full interactive tools that operate on your data.
Isomorphic components
One of CoCube's key design goals is that your components shouldn't need to change when you go from working alone to working with others. A component that works on local data works identically when that data is shared with a collaborator. This is what isomorphic means in CoCube: the same component definition works across local and collaborative contexts without modification.
Why this matters
In many systems, adding collaboration means reworking how data is stored, accessed, and synchronized — replacing local state with server calls, adding conflict resolution logic, or redesigning your data model for multi-user access. CoCube avoids this entirely. Components read from and write to datums through cell expressions and event handlers. Whether a datum is local-only or synced with five collaborators, the component interacts with it the same way. The collaboration layer (Loro CRDTs and WebSocket sync) operates beneath the component system, invisible to your component definitions.
From local to collaborative
Suppose you build a task tracker for yourself: a list component that displays tasks from an array, a text input for adding tasks, and a button to mark them complete. All of it runs locally. Later, you decide to share it with a colleague. You sync the workspace and its underlying datums to the server, then add your colleague as a collaborator. The components don't change. Your colleague sees the same task list, can add tasks and mark them complete, and every change syncs in real-time. The transition required zero changes to any component definition.
Loro and CRDTs
CoCube stores all datums using Loro, a conflict-free replicated data type (CRDT) library. Every datum — from a simple text value to a full component definition — is an Loro document under the hood.
What is a CRDT?
A CRDT is a data structure designed for distributed systems. It guarantees that multiple copies of the same data across different devices converge to the same state when changes are merged — regardless of the order those changes arrive.
Why Loro?
Loro supports rich data types (text, numbers, arrays, maps, and nested structures), handles concurrent text editing with character-level merge resolution, tracks the complete change history of every document, and produces compact binary sync messages. For CoCube, Loro provides the foundation for both local persistence and collaboration. Locally, each datum is stored as a binary Loro blob in your SQLite database. When you sync, Loro generates incremental sync messages sent to CoCube's servers. The server merges these and forwards them to collaborators, whose clients apply the incoming changes and persist the result.
Offline editing
Because CRDTs don't require a server to resolve conflicts, offline editing works fully. You can edit while disconnected, and when you reconnect, your changes merge correctly with anything collaborators did in the meantime — even after extended offline sessions spanning days or weeks. This is central to CoCube's local-first design. Your data is always available and always editable, regardless of network connectivity. The server is a convenience for sharing and syncing, not a requirement for using the system.
SQLite: your local database
CoCube stores datums locally in a SQLite database — a widely used, open-source embedded database engine that serves as the durable storage layer for all your local data.
How it works in the browser
CoCube compiles SQLite to WebAssembly and runs it directly in your browser. The database uses the browser's Origin Private File System (OPFS), a modern web standard that gives web applications a private, persistent storage area. OPFS is scoped to CoCube's origin, isolated per browser profile, and inaccessible to other websites. The database runs in a Web Worker (a background thread), so reads and writes never block the UI. Each datum is stored as a row containing the datum's unique identifier and its Loro binary data. When you open CoCube, datums are loaded into memory — from that point, the in-memory copies are the primary working copies and the database acts as a durable backup, kept in sync with every change.
No account required
Because your data lives locally, CoCube doesn't require a user account for local work. You can create datums, build components, and use them without signing in. Your data stays on your device unless you explicitly choose to sync.
An open format
Both SQLite and Loro are open-source and use well-documented formats. Your data isn't locked into a proprietary system. If CoCube's servers went offline permanently, your local data would remain on your machine in a standard SQLite database with datums in Loro binary format.
For a complete explanation of how datums are stored, synced, and shared, see the Data Storage page.
Components in depth
Creating a component
- Navigate to the console.
- Click the Create Component button to create and start editing a new component.
- With the editor open, you should see a single component rendered as a green square. This is how a component looks when no cells are defined.
See Create: Interactive Bar Chart for a step-by-step walkthrough of creating an interactive chart component from scratch.
Modify appearance
Let's change the color of the new component.
Create a new cell named Color. Capitalization matters — Color is different from color. As you type the name, you'll see a pop-up listing common attribute cells. Feel free to select Color from the list.
Once created, the cell will contain a default Color value. Click it in the cells panel to open the expression editor. Since we're editing the Color cell, we want it to contain a Color type. You can click this value to open the color selector.
Attribute Cells
Attribute cells define how the component looks and behaves. They are cells with special names, listed in the table below along with the type of data they expect.
| Color Color | Color of the component. |
|---|---|
| Width Number | Width of the component. |
| Height Number | Height of the component. |
| Rotation Angle | Rotation angle of the component. |
| RotationAbsolute Bool | Set to prevent a component from inheriting its parent rotation. |
| Display Shape | Define what the component looks like (square, letter, etc). |
| AlignTo FixPoint | Set where the component aligns to. |
| Origin FixPoint | Set the point on the component which should act as the center. |
| Anchor Anchor | Define what the component is aligned to. |
| Animation Map | Define the animations for the component. |
| EventHandler EventHandler | Define how the component reacts to events. |
| Components Map | Define subcomponents. |
| Selectable Bool | If true, the component will be selectable by the event system. |
| PivotX Number | Define the pivot point offset in the X direction. |
| PivotY Number | Define the pivot point offset in the Y direction. |
| OffsetX Number | Define the component offset in the X direction. |
| OffsetY Number | Define the component offset in the Y direction. |
| ClipMask Bool | If true, the component will act as a clip mask. Subcomponents will only render directly behind it. |
| Layer Bool | If true, the component will be displayed above all other components. |
Combining components
To add a component inside another component, use the Components attribute cell. Here is a component containing two subcomponents named Little and Big.
| Components |
| ||||||||||||
| Height | 50 | ||||||||||||
| Width | 50 |
The easiest way to add subcomponents is through the Components panel. You can also set the Components cell directly. This cell expects a Map containing Components and/or Views.
To reuse an existing component, add it as a subcomponent using a View.
Nested component layout
Subcomponents are positioned relative to their parent.
By default, the center of a subcomponent aligns to the center of its parent. There are several ways to adjust this.
Set the AlignTo and Origin cells to control alignment.
Set OffsetX and OffsetY to add a positional offset. These cells expect a Number.
The PivotX and PivotY cells adjust the rotational center point. These also expect a Number.
The Anchor cell controls what the component is anchored to. By default, subcomponents are anchored to their parent. This can be changed to anchor to the screen or the workspace. This cell expects an Anchor.
Overriding cells
Overriding cells is how you customize generic components for a specific use case.
Select a component in the Components panel to open it in the component editor. If the subcomponent is a reference (View), modifying its cells sets overrides for that specific reference only.
Selecting a child component that is defined directly (a Component, not a View) edits the nested component itself, not overrides.
Views
A View is how you use a component. It is a reference to a component, paired with a set of cell overrides. Views are the mechanism that lets a single component definition be reused in many different contexts, each with different inputs.
How views work
When you add a subcomponent to a parent through the components panel, CoCube creates a View that references the subcomponent's definition. The view can include overrides: values that replace specific cells in the referenced component without modifying the original definition. This is how a generic component like a bar or a button becomes a specific bar with a specific height, or a specific button with specific text.
For example, a Button component defines what a button looks like and how it emits events. To use it in your component, create a View that references Button and override the cells defining, for example, the text to display or the event tag to emit when it is clicked. The button definition stays unchanged. Your view customizes it for one specific use.
Views pass inputs to components
Views are how components receive inputs. The Components attribute cell on a parent expects a Map of View and/or Component values. Each View in that map carries its overrides, which are applied when the referenced component is rendered.
This pattern scales naturally. A dashboard component might include a chart, a filter panel, and a data table, each with overrides that connect them to the same data source. The chart, filter, and table definitions remain generic and reusable. The views in the dashboard are tailored for that particular use case.
Cells and References
Cells can reference other cells using a few built-in functions.
Here is a component that uses a reference to compute its size dynamically:
| Height | Cell "Width" |
| Width | 50 |
Changing the Width cell causes the Height to update as well. In this case, the height always matches the width.
To read a cell from a parent component, use:
This reads the Width of the containing component. For example:
| Components |
| ||||||
| Width | 100 |
Any change to the parent's Width cell will update the subcomponent's Height.
Adding Reactivity
Components can respond to events such as clicks and key presses.
To make a component reactive, set the EventHandler cell to an EventHandler. You can then configure the event handler in the expression editor.
Event handlers use two concepts: Matchers and Actions.
A matcher specifies which events the component listens to. For example, to respond to a left click, the matcher would be:
Once a matcher is selected, you can add actions. Say the component's Color cell should change on click. Add the following action to the event handler:
When the matcher fires, the action runs, and the component's color changes.
SetCell accepts any expression. The expression is evaluated when the event matches.
Each matcher can have any number of actions.
You will also see an option to bubble or capture the matched event. Capturing stops the event from propagating to parent components. Bubbling lets the event continue up to parent components, where it can be matched again.
Emit tags and emitted data
You will often want to emit custom events from a component. Use the Emit action for this.
This action takes two arguments:
- An expression that evaluates to an event tag (Text).
- An expression that evaluates to any data you want associated with the event.
Define a matcher that matches the custom event tag. Use the Matched function to retrieve the matched data in an action.
Collaboration and publishing
When you're ready to share, sync a datum to CoCube's servers and make it public. Synced datums are stored both locally and on the server, enabling multi-device access and real-time collaboration. Making a synced datum public tells the server to serve it to anyone with the URL. Viewers can interact with the component just as they would in your workspace — they just can't modify its definition. Since components and workspaces are themselves datums, publishing works the same way for all of them: sync, make public, share the link.
For deeper collaboration, add collaborators to a synced datum for real-time read and write access. Changes merge automatically using Loro CRDTs, so multiple people can edit simultaneously without conflicts. For a detailed explanation of datum states, sync mechanics, collaboration, and data security, see the Storage page.