Building web components for multi-tenant environments

Pawel Uchida-Psztyc
8 min readDec 3, 2020

In the majority of cases, you write your code for your application that is running on your server(s). Or as a Kubernetes image running in GKE (Google Kubernetes Engine) or another platform. Sometimes, however, you run your application alongside other applications. This may happen in an enterprise environment, ERP/CRM/other systems, or even inside your organization when your application or part of some logic runs in another application. This situation may cause some trouble when not prepared. In this article, I’ll point out all the common issues I have encountered while running my applications based on web components in an enterprise environment.

Name collision

First and (I think) the most important issue you will ever face in a multi-tenant environment with web components is the name collision problem. You see, a web component can be registered only once in a browser’s global registry. This means that a “paper-button” web component offered by Google can be included into your page view only once. Should another application try to register a component with the same name, the operation will fail. What can be more surprising for some, even the same component, with the same version, and the same code base will throw an exception if registered again. It’s not hard to imagine a situation when two applications are running inside a single page. Both are made with the same components, coming from the same source, but have separate build processes. This would make a code duplication that browsers really don’t like. What does this mean for your application? Whichever application first downloads the source code and executes the bundle will survive. The other one won’t be so lucky as the bundle will throw an error, and the entire application will collapse. In this situation, you would see only one application is actually running and an error from another in the developer console.

Few caveats here.
- A component does not check whether it is already registered in the custom elements registry. Doing so may lead to a potential security problem described below.
- You bundle your application separately to another application running on the same page. When a single build is made for the final application containing sources of both, then bundlers will solve this problem for you.
- You don’t prefix your components to minimize the risk of duplicated names. This does not solve the problem using other popular component libraries.

What can be done in case of a name collision? First of all, if it’s possible, bundle all applications running on a single page together. Popular bundlers have tree-shaking included, and this ensures that a component is included only once. Secondly, make sure that all applications are using the same version range of the components. NPM is not very web components friendly, and it does not understand that your application can only use a single version of a component. Even when you bundle both applications together but both are using a different major version of the same component, a name collision will occur.

You can also not bundle your application at all and serve everything on the HTTP 2 protocol directly from sources. This, however, would have to be measured by you if this causes performance issues. Technically you are transferring the same amount of code. You can employ some form of server-side optimisation to minify the code to reduce the transferred payload. HTTP 2 allows you to transfer multiple files on a single connection with a minimal number of headers (which can grow big over a huge number of files) and without the whole transport negotiation for each file.

If all of the above is not possible because, for example, another application is not controlled by your organization or your team, there are two more options but less likely to be used today.
The first one is to use some method that registers a random name for a component for you and updates your templates to use the auto-generated name of a component. Folks from the open-wc initiative have a mixin function that does exactly that. It’s called scoped-elements. Instead of importing a file where the registration actually takes place, you import the source class, and you pass it as the value to the “scopedElements” getter.

import { MyComponent } from '@package/component';class MyElement extends ScopedElementsMixin(LitElement) {
static get scopedElements() {
return {
'my-component': MyComponent,
}
}
}

Now each time you use the <my-component> in your template, it will be replaced with whatever name is generated by the mixin function. This comes as a cost. The application may be slower about 2 per cent compared to not using this method (according to authors of the mixin). Also, if you use element names in your CSS, it won’t work either as the names are replaced with a random value.
The other method to work with name collision is to use the scoped custom elements registry. At the moment of writing this article, it is still just a proposal, and there’s no actual implementation of this web platform feature. According to the discussion under the proposal, the author can create their own custom elements registry and register the component within it. This registry is not accessible by others. You need to pass a reference to the store to be able to register a component within the store. This method would require some significant changes to the existing components registration system but would allow running web components in a multi-tenant environment without worrying about name collision.

Component highjacking

In environments where independent applications are running inside a single document, it is possible, in specific circumstances, to register a component with the same name so the other application would use this component instead of trusted own component. Say, a custom input element.
I want to emphasise that this is not something that would normally happen. You have to create conditions for this to work.

Lets first track how component highjacking works. Say you have two applications working in a single document. Both applications are published in a marketplace offered by the hosting application, that runs them both. The user first installs the application A that is a genuine application. Then the user installs the application B that is a bad actor. The evil application tries to highjack the input component from the application A to “steal” user input from the other application. Now, when this happens in the order described above(installation of the A application and then B), then nothing really happens. Application A already registered its own component, and the application B can’t do much about it. But after reloading the page, by chance, the application B can register its component first, before the application A will get a chance to do the same. Theoretically, with the right conditions (or rather bad practices), application A will ignore errors during own component registration and continue working. Meanwhile, each keystroke inside the custom input field in application A is recorded by the fake application B. It’s scary, I know. But like I said you have to do some work to make this even possible.

First of all, you are ignoring errors when registering the components. This is a bad practice and should never be done by a professional programmer. Errors indicate that something is not right, and this can’t be ignored. Even at the cost of a user seeing the error. This is also information for the user that something is not right, and possibly an issue should be reported.
Secondly, the generated bundle of the application splits the code into chunks but in a way that the dependencies are loaded from one file and the application logic from another, independently. So when in one file there’s an error the other will be executed regardless of the error. This is not only bad practice as this ignores errors; it also means that something is off with your bundler configuration. A bundler that uses tree-shaking pack the code depending on the usage and not a dependency-source relationship so it can produce the most efficient bundle. The by-product of that is when an error occurs in the bundle, then the entire application crashes.

So how to protect yourself from this situation? There’s really no valid option here, except for the future scoped custom elements. Don’t ignore errors, and don’t prevent application crashing when something is wrong. Let application fail, inform the user, and hope that they will report an issue. You will never learn that something is wrong by yourself in your test environment. Not in this case. Instead, let your users help you to understand what is happening.

Again, component highjacking is not something that would normally even be possible. In most cases, after a component has been highjacked, the genuine application would crash. You should make sure that your application crashes in such a situation, and you are not creating an environment for this security vulnerability to materialize.

Global custom DOM events

Your components communicate with the outside world via DOM events. This is how the entire web platform works (well, React has a pattern of passing functions as callbacks instead of dispatching events, but I consider this a terrible practice). It is tempting to make events “global” by enabling bubbling through the shadow DOM boundaries. In a multi-tenant environment, this means that any application can listen to your events. If the event is just simple “change” without any data in the “detail” property of the CustomEvent it might not be very harmful (though, such an event shouldn’t bubble in the first pace). But when you are transferring a state object through an event that may open a vulnerability in your application because other applications running in the same environment can read this event. With the right architecture, global events are rarely usable. Try not to build your components this way. Alternatively, you can stop the propagation of any event on the top-level application. If global events could be an issue for your application, consider changing the architecture to pass the sate through a child-parent only communication channel, and not through bubbling events.

Customization of the application/component

This is less about the multi-tenant environment as this would happen regardless when you share your component with others. Most likely, the consumer of your component will need to customize it somehow. Often it is limited to theming the component to match another environment or user preferences. Today, the only reliable option of allowing users to customize your components are CSS variables used in your component's view (the CSS definition). I personally, after building 3 ecosystems of web components, got used to automatically declaring a CSS variable when defining a colour for practically anything (font, background, border, shadow, etc.). You can’t predict every user preference, and it is not practical to create variables for everything (probably performance suffers too). But defining colours as variables is a good start. To make sure you have defined all variables that the consumers of your component would need, create a default (or light) and a dark theme for your component. If you can manage both through CSS variables only then, you most probably are giving the consumers of the component all they need.

--

--

Pawel Uchida-Psztyc

Design, APIs, front-end, strategy, product, and educator. I work for big tech but I share my personal views and opinions.