Built with Webstudio

Webstudio's Architecture — an overview

October 1, 2023
15 min read time
Author:
Oleg
Futuristic looking article image. Credit: Milad Fakurian via Unsplash

This is an introduction for developers who want to contribute to Webstudio Core. However, it may also be an interesting read for ambitious designers who want to better understand the future of Webstudio and Visual Development.

While it's impossible to dive into every aspect of the architecture, since things are still shaping up, I will walk you through the core aspects:


1. Alignment with the web

Finding the perfect level of abstraction is the key to the success of Webstudio. The goal is to enable designers to understand the constraints of the underlying platform without being overwhelmed by technical details.

How do we achieve the perfect balance that doesn't neglect the nature of the platform while empowering designers?

Web Foundation

The reason for preserving lower-level HTML and CSS semantics is to enable transferable knowledge. Learning Webstudio should feel like learning the web platform itself. Understanding the platform's limitations is vital for long-term involvement, as you will encounter increasingly complex use cases. Your ability to comprehend the inner workings of everything will become increasingly critical. This stands in contrast to tools that abstract the platform to a level where it becomes impossible to gain a comprehensive understanding.

CSS Typed Object Model

Supporting the entire CSS from a visual style panel is not feasible. Therefore, we had to come up with a way to provide an authoring experience for the remaining CSS that cannot be accommodated within the style panel.

We have created a JSON-serializable data structure, similar to Houdini's object model, that allows us to have fully typed access to CSS values. This is the foundation for a consistent data structure and will allow us to implement a devtools-style CSS editor with bi-directional synchronization with the style panel.

const styles = [
{
"property": "padding-bottom",
"value": {
"type": "unit",
"unit": "px",
"value": 10,
},
},
]

Design Tokens instead of classes

Design Tokens are a core part of the architecture. They are conceptually aligned with the w3c Design Tokens Spec Draft and allow us to avoid any specificity issues in your work. Furthermore, you can specify local styles, allowing you to quickly experiment without naming anything and only start organizing when you want to clean up and make styles reusable.

Design Tokens, local styles, and tag styles all fall into the category of "style sources". Technically, all of them are just "mixins". You can imagine them as a list of style blocks being merged into one style block in the exact same order as you see them in the style panel:

const token1 = {
color: 'red'
}
const token2 = {
display: 'block'
}
const token3 = {
fontFamily: 'Arial',
fontSize: 12
}

// Final merged style block
const style = {...token1, ...token2, ...token3}

Once merged and compiled into CSS:

[data-ws-id=WKJAXi] {
color: red;
display: block;
font-family: Arial;
font-size: 12px;
}

2. Native extensibility

The ultimate goal of visual development is to work with the same components used in production. This eliminates the need for guesswork, as is often the case with tools like Figma, ensuring that the final product looks and feels exactly as desired.

Real components on the Canvas

One major goal of the architecture is to enable the use of libraries that developers already use in production. This is important to avoid starting from scratch and wasting resources, as we, the developer community, have already invested a significant amount of time and effort in building powerful and accessible libraries.

Additionally, developers should be able to bring their own custom components. Our first Proof of Concept is the integration of Radix UI.

Although the metadata API for integration is currently unstable, here is an example of how it looks today:

export const meta = {
category: "general",
type: "container",
label: "Box",
description:
"A container for content. By default, this is a Div, but the tag can be changed in settings.",
icon: BoxIcon,
states: [
{ selector: ":hover", label: "Hover" },
{ selector: ":focus", label: "Focus" },
],
presetStyle: [
{
property: "boxSizing",
value: { type: "keyword", value: "border-box" },
}
]
};

export const Box = (props) => <div {...props}/>

To make integrating components easier, we generate arg types from the component’s TypeScript types, so that you have a minimum amount of configuration.

Framework agnostic future

Our SDK consists of several packages:

  • “sdk” - a set of utilities every SDK will need, no matter if written in React, Vue, or anything else.

  • “sdk-components-react” - primitive React components like Box or Button

  • “sdk-components-react-radix” - Radix UI components

  • “sdk-components-react-remix” - Framework specific components e.g. Form or Link, could be replaced with Next.js

The goal is to enable the community to create SDKs for each framework and allow companies to create their own internal SDKs.

One component away from anything

Once it's clear that everything on the Canvas is a component, it becomes obvious that you can build anything with it.

For example, you could create an integration with components specialized in building emails. This way, you could visually construct an email that would work for all email clients as long as the components handle all the implementation details.

In fact, I can't wait for an email builder SDK to be completed so that I can stop using the terrible visual builders offered by every email service.

Sandboxing on Canvas

The goal is to allow 3rd party components on the Canvas in a way that can be safely contained. We use an iframe with postMessage as a communication layer and combine it with Nanostore subscriptions and JSON patches to sync the states and data between the Builder and Canvas.

Plugins

Recently, we have begun working on the Plugins API. Similar to the SDK components, our objective is to empower the community to develop plugins. The Plugins API is being implemented using the same technique as the canvas: iframe and reactive stores. It offers the following capabilities:

  1. Access the entire project data.

  2. Receive reactive updates, just like on the Canvas.

  3. Integrate inside native panels, such as the left sidebar, right sidebar, and page settings.

  4. Utilize the same design-system components that we use to build the Builder UI.

Preparing for a marketplace

The main advantage of building Webstudio as an Open Core platform is the ability to create a marketplace. Developers should face minimal obstacles when sharing plugins, UI kits, components, templates, and other resources, thanks to the architecture that provides full isolation for 3rd-party code on Canvas and the published site.

With this approach, designers on Webstudio will have an unprecedented level of control and access to the entire web ecosystem of services, while maintaining a balanced level of complexity.

One example that we envision is a developer creating an integration with Shopify. They provide a plugin and SDK components. The plugin allows for settings, while the components have built-in behavior and default styles. During server-side rendering (SSR) in Cloudflare Worker, the components can fetch data from the Shopify API, cache the responses, and render the results. Once the results are cached, the storefront loads as fast as a static site. However, it still functions as a server, handling requests from the UI and communicating with the API backend.

Designers simply need to drag and drop components onto the Canvas and customize the look and feel of fully functional components.

Of course, there is still much to explore regarding the challenges of running a marketplace and dealing with potentially malicious actors. Exciting times are ahead!


3. Decoupled publishing

One of the key features in the architecture is the decoupling of the Builder and the published site or app. The hosting requirements for the Builder are likely to be much more complex than those for the project being built. Therefore, it is important to separate these aspects to avoid ending up in a situation similar to WordPress.

When building in Builder UI, we send patches to the server and store the data as a JSON blob that looks like this:

[
{
"type": "instance",
"id": "zWscRYTwozwV0rjcXPmJ_",
"component": "Paragraph",
"children": [
{
"type": "text",
"value": "Hello world"
}
]
}
]

To publish your project, first, we need to install the necessary dependencies. Then, we use Webstudio CLI to download the required data and compile it into components. After that, we scaffold a framework-specific app, generate routes, and generate CSS.

Once the app is ready, it can be deployed anywhere. To publish it to the cloud, we use a cloud-specific CLI. When publishing from Webstudio Builder, your app will be deployed to Cloudflare Workers using a separate Worker for each project. This ensures safety by preventing malicious scripts from accessing other projects.

Workers are amazing in terms of performance, scalability, and cost, a true revolution in infrastructure.


4. Realtime collaboration and offline-first

Our ultimate objective is to create an offline-first app that offers the real-time collaboration quality found in Figma, with a target performance of 30FPS or higher. While we are developing for the web and recognize that other services may not function offline, we should not be hindered by limited or unstable internet access.

Implementing an offline-first architecture, even for an application primarily used online, brings benefits such as improved resilience, cost-efficiency, and future-proofing. However, before moving forward, let's address some of the main challenges we have already overcome.

Reactivity

Building Webstudio Builder without reactivity would be challenging. The Builder needs to frequently update large tree structures at different levels, which React's pure top-down architecture is not suitable for. Attempting to use React in this scenario results in endless optimizations through memoization.

To address this, we are taking a mixed approach: signals + vdom. Nanostores provide a signals-based mechanism that, when used in React, still adheres to React's render cycle. However, reactivity alone is only a small part of the solution.

JSON Patches

Our challenge is the synchronization of potentially large trees while maintaining real-time collaboration. Sending the entire data is not feasible in this scenario. To address this, we utilize Immer.js - a library that allows us to work with plain JavaScript data structures and automatically generate JSON patches in the background. This approach provides not only excellent developer ergonomics but also enables time travel by keeping track of changes and allowing for revisions.

We use JSON patches to synchronize data between Builder and the server, Builder and the Canvas, and soon also between Builder and plugins.

// Patch example
[
{
"op": "replace",
"path": ["6zwJnT5YtKuKGJsD3zBCY"],
"value": {
"type": "instance",
"id": "6zwJnT5YtKuKGJsD3zBCY",
"component": "Paragraph",
"children": [
{
"type": "text",
"value": "Some updated text"
}
]
}
}
]

Undo-redo

Undo-redo is another significant challenge, in particular in combination with many different data structures that need to be updated as a single transaction and synced to multiple destinations.
To solve this we built Immerhin - a library on top of Immer.js and Nanostores that leverages JSON patches to provide undo-redo and a transactions interface.

Real-time collaboration

To be completely transparent, we have not yet achieved full real-time collaboration. We still need to address conflict resolution between changes made by different users and establish a high-performing network and subscription mechanism on a global scale. Fortunately, we do not have to build everything from scratch, as projects like Partykit have already solved these challenges using Cloudflare Durable Objects. In terms of using CRDTs, our approach is closely aligned with Figma's approach.


If you would like to learn more, please let me know, and I will be happy to provide additional details in this post. I am impressed that you have made it this far, and I encourage you to reach out to us on Discord or Github for potential collaboration.