Skip to main content

How the Dashlane Extension Is Getting More Modular with NestJS

  |  Tristan Parisot

Dashlane is using NestJS, a framework for building efficient Node.js server-side applications, in our web extension. In this blog post, I explain why and how we made this decision.

Creating a web extension’s architecture is an iterative process. In a living product like our web extension, just like in a city, building infrastructure takes time.

To allow the Dashlane web extension to evolve in the ever-changing web-extension ecosystem, we needed a more efficient technical organization. Thus, we decided to adopt NestJS, making it a critical part of the “urbanization process” of our extension.

Overview of an extension

An extension is composed of components communicating through a background page. The communication is done via messaging. Compared to the usual web world, there’s no way for these parts to communicate through web requests.

The background node must be as light as possible because it’s frequently reloaded.

A story about Manifest V3

Google holds 84% of the market share of our users’ browsers with Chromium-based Chrome and Edge. They are changing the web extension API to move away from a persistent background page to a service worker whose lifecycle is controlled by the browser. It’s not more persistent and can be terminated at any time to save resources. This is the main change brought about by Manifest V3.

Dashlane too must follow Manifest V3. Take a peek at what we did for MV3 in our previous blog post about it.

Because we needed to refactor our code to leverage lazy loading and state management enforcement practices, we decided it was time to re-write our architecture.

What did we do?

In web pages and the pop-up, we have a layer that forwards calls to a business layer. This business layer can be in the same context as the UI, or it can be in the background. 

We introduced a central messaging layer between the UI and application modules. Since modules are bounded contexts, it makes sense to apply domain design in naming conventions. We therefore used the CQRS pattern instead of CRUD.

Application modules can also use this CQRS layer.

The CQRS layer dispatches the query in either a module hosted locally, such as in the web app, or hosted in the background.

We apply the CQRS pattern for the extension with the following specialization:

  • A command is the expression of the intent of the user. Example: “As a user, I want Dashlane to create a new password for me.”
  • A query is the ability of the system to inform the user. Example: “As a user, in order to fill in my login form, I need to know which accounts I have on this website.” 
 A rectangle labeled “Background” has 3 different rectangles around it with arrows pointing to it. The rectangles are labeled: “Popup,” “Content pages,” and “Web-app.” At the bottom of the image it says “Extension = multiple nodes.” Caption: The background node must be as light as possible because it’s frequently reloaded.

What is NestJS and why do we like it?

NestJS is a framework to develop service-based applications. When a request arrives, it triggers a pipeline and dispatches said request to its handlers, which provides a service.

NestJS has two functionalities we need to achieve modularity:

  1. Dependency injection
  2. Request handling

Shipping NestJS in a web application

NestJS is supposed to be a server-side framework. However, we use it in the background page of the extension, which isn’t a web server and runs on a navigator JS engine, not in NodeJS.

Shipping NestJS in a browser requires some polyfills for processes, buffers, and more, but it works well. See this GitHub page for an example of a NestJS app on a webpage using Webpack.

A reactive implementation of CQRS

Our primary UI technology is React. Our requirements for a UI system are to:

  • Display to the user what they need to know
  • Accept user intents

These two cases are what we call use-cases. For them, we apply the CQRS pattern because it allows domain-driven naming and separation of intent versus data reading.

Commands and queries are what we call “use-cases.”

All use-cases stream multiple values, which can be a success, a functional failure, or an exception. The primary result type of use-cases in JavaScript is therefore:

Leveraging NestJS pipelines

At Dashlane, we were looking for a solution that could enable us to:

  • Split monoliths into testable chunks via dependency injection
  • Disassociate use-cases from their implementations
  • Have per-request scoped data and per-user scoped data

At first, we were looking at ts-syringe, but NestJS offers nice features:

  • Per-request scoped providers
  • Requests, pipelines, and controller logics

However, some points are not adapted to us:

  • NestJS is based on web-requests, and web-requests are one-shot.

So we bridged use-case to handlers using NestJS.

A flow chart shows 2 boxes at the top in a yellow box labeled “NestJS world.” One box is labeled “HttpServer” and the other is “Base Controller.” A box with “CQRS ‘Http’ server” has a dotted arrow pointing up to the “HttpServer” box, a solid arrow pointing left to a box labeled “CQRS broker,” and a solid arrow pointing down to a box labeled “Use-Case request, response” box. That arrow is labeled “subscribes.” Beside the “CQRS ‘Http’ server” box is a box labeled “Cqrs Controller” with a dotted line pointing up to the “Base Controller” box, a solid line pointing down to the “Use-Case request, response” box, and a solid line pointing to the right to a “Query Handler” box. Above that box is a box labeled “Command Handler,” and below it is a box labeled “Event Handler.” The “Command Handler” and “Event Handler” boxes are connected by a solid line with an arrow on both sides, and the arrow is labeled “Instantiates, calls.” The “Command Handler,” “Query Handler,” and “Event Handler” boxes are all placed in a large green box labeled “In a module.”

As you can see, NestJS has an HttpServer abstraction that isn’t ideal for us because we have no real web server in the web extension. At the time of writing this architecture, the micro-service also required some adaptation. We went with a fake HTTP server.

NestJS dynamic modules

One thing we need to be cautious about with NestJS is the boot time. At Dashlane, in Manifest V3, we need to boot the application fast when something happens. Essentially, we want to autofill web pages without making the user wait. We did some benchmarks (at the time, on NestJS 8) and found that the boot-time of the NestJS app is proportional to the number of modules.

​​

A line graph labeled “Time (ms) vs. Modules) shows modules ranging from 0 to 50 on the X axis and Time (ms) ranging from 0 to 200 on the Y axis. A blue line at (0,50) on the left moves steadily upward, ending at (50,155) on the right.

However, we identified the source of the slow-down: A method is called to tag an identity token to all modules to give them unity tokens. As of the redaction of this document, we do not yet have enough modules to suffer from the announced slow performances.

This benchmark needs to be redone, as promising improvements have been merged to NestJS-9, such as !11023. If these aren’t enough, we still have the option to investigate building these tokens on build-time rather than execution time. 

Meanwhile, we do not use the dynamic modules as they take longer to start. Instead, modules can get their configuration by declaring a configuration dependency injection token that will be provided by the start of the extension.

For example, we have an anti-phishing module that depends on a set of services from a Remote-file-update module, which injects a configuration dynamically:

This replaces the usual way of Nest for doing dynamic modules:

Module static configuration is passed with a special configuration property on our NestJS wrapper. This allows passing it at the application entry point:

Per user singletons

We plan for the Dashlane extension to support multiple accounts, letting users have a professional and a personal account. At the time we built our framework using NestJS, durable providers didn’t exist. We ended up making a factory with a cache to create a service per user. Modules needing per-user singletons are doing this with an async provider:

The userScopedSingletonProvider adds a hidden cache and makes sure the factory is called only for users whose services are not in the cache.

We may switch over to multi-tenant, but this isn’t yet planned.

State management

All services and providers should be stateless in our application. This is due to one of MV3 constraints: The application can be killed at any time. Stateful components should be limited to what we call Stores. Stores are declared like this:

They expose a set method and a state$ observable stream. When updated, our framework will seamlessly encrypt the store content using a key specific to the user (the key can only be decrypted when the user logs in with SSO or a Master Password).

The stores will be constructed and read-only when needed using NestJS dependency injection (and our singleton layer).

On MV3, Stores use the storage.session web extension API to allow modules to quickly resume their boot phase without loading data from local storage (which is costly due to decryption).

CQRS API and Handlers

We decided not to leverage NestJS CQRS features for two reasons:

  • Nest JS CQRS Queries still return a single value. We want a stream in our application. When given observables, NestJS only uses the latest values of them.
  • It brings the notion of aggregate-root. As of today, we (Dashlane web framework maintainers) are encouraging devs to isolate code using handlers, which is already a change of philosophy. The added complexity of Aggregate Root was not our priority.

A module CQRS API is described declaratively in contracts:

Command and queries are classes defined as such:

When declaring a module, we bind the CQRS contracts to handlers in the Module decorator:

Here, handlers are special providers that can handle command, queries, or events (but that’s not covered in this blog post):

Using ModuleRef to create handlers in our controller

In our controller, we’re using the ModuleRef object provided by Nest to instantiate handlers manually. When a “HTTP query” arrives, our framework knows the handler type for it and instantiates it:

Wiring queries to the UI

Devs are provided with a helper React-hook useModuleQuery that sends the query through the CQRS broker, then Nest pipeline:

The react hook provides a real-time, reactive stream on the module. Under the hood, it uses a React context to get a CQRS Broker to trigger the NestJS pipeline. The value obtained can be sent to a prop of a component.

Conclusion

Using NestJS in our application has provided benefits to our developers: A lot less boilerplate is needed to wire handlers to the view (only API declaration and handlers). It also helps in mocking module data during tests.

Since we have our own framework that’s wrapping NestJS primitives, we should, in theory, be able to change NestJS for something else if we wished to.

What’s next for Dashlane and NestJS? 

  • Evaluate bootstrap performance in NestJS 9
  • Evaluate multi-tenancy
  • Evaluate micro-service approach
  • Evaluate nullable-pattern feasibility in NestJS for integration testing 

Questions? Comments? Reach out to us on Reddit, Linkedin, Twitter, or Facebook.


Technical references

Do you want to dive more into the technicalities? Our web extension is not open-sourced yet, but you can look at the specs of what we use:

Sign up to receive news and updates about Dashlane