Editorial note, April 2021: This document was produced in January/February of 2020. It is an early exploration of frontend plugins, and an attempt at assigning unambiguous terminology to a complex domain. In that sense, it posits plugin types that we may not actually choose to support.
Our ability to extend our platform - without modifying it - is a prerequisite for innovation and experimentation, and allows us to maintain a small, stable core where volatile features remain in the periphery.
Frontend pluggability enables loosely coupled, lightweight platform extensions focused on user-facing features. It goes hand-in-hand with our micro-frontend development efforts, endeavoring to bring the speed, ease of development, and modularity of our MFEs to plugin and extension development outside the core.
Furthermore, in the same way that micro-frontends allow us to use client-side integrations to easily fetch data across contexts (LMS, Ecommerce, Discovery, etc.), frontend plugins allow us to securely and maintainably expand that capability to externally hosted and operated systems.
What about LTI?
Whether or not to make use of LTI is an important consideration for frontend pluggability, as application/plugin interactions are right in its wheelhouse. In this document, we take the approach of working up toward full LTI capabilities.
Through some careful consideration of our use cases, we’ve decide that fully supporting the LTI specification is not necessary to enable a broad range of functionality for our plugins, but will ultimately be an important component of our framework. We’ve determined that nothing about our approach is specifically at odds with LTI’s specification, and so it feels like a natural extension of our work when the time is right.
With frontend plugins, we hope to enable the following:
A rich, dynamic ecosystem of innovative teaching and learning extensions, tapping into existing and future LTI tools.
Simple, secure experimentation for course teams and operators.
Clarity and maintainability for platform developers.
Greatly increased velocity and flexibility for plugin developers.
Ultimately, better outcomes for learners.
Plugins may - almost by definition - take many forms. We have a few specific examples in mind which may help illustrate the scope of our proposed plugin framework, as well as some broader considerations for where we’d like to go with frontend pluggability.
As our most near-term project, a plugin to enable context-specific discussions has greatly informed this document. This would involve a discussions widget which can be loaded on different sequences in a course, providing students with forums to discuss the material.
The discussions plugin would need to be aware of which content it’s attached to in the course. Furthermore, different course teams may want different discussions plugins, so we’ll need to be able to configure the discussions slot on a per-course - or perhaps per-program! - basis.
The plugin will clearly need to store student discussions somewhere, necessitating the existence of some sort of backend for it to talk to. If that location is in a separate data store, it will need a reliable way to associate comments and posts with particular users, necessitating the use of anonymous user identifiers to alleviate PII (personally identifiable information) concerns.
Content Recommendations Plugin
A plugin allowing a recommendation engine to suggest content for a user as they navigate through a course. The plugin may appear in multiple locations on the page (we have multiple sets of navigation buttons). The plugin will need to be able to ask the page to navigate to a different part of the course, and so may need detailed course hierarchy data. It may also want to gather data on a user’s performance in the course to help inform its engine.
Today, xBlocks are effectively backend plugins. There are dozens (hundreds?) of xBlocks in the world. For certain classes of xBlocks, they may be better and more easily implemented as frontend plugins. Implementing this would require us to be able to serve frontend plugin content at the “component” level of course content, rather than rendering the whole unit server-side.
In the long run, giving our frontend plugin framework the ability to interoperate with existing and future LTI tools would be a valuable addition. LTI gives us a well-established framework for application/plugin communication and with which to perform authentication, allowing plugins to securely interact with our existing Platform APIs, extending their capabilities. There’s a lot we can do without LTI, but it’s likely the preferred way for us to handle authenticating externally hosted plugins in many cases.
This section defines various concerns in developing our plugin framework, and serves as a broad breakdown of potential features, most of which are necessary to fulfill the above example plugins.
Plugins are code that is not bundled with an application, but is built/deployed separately and sandboxed in an iframe at runtime. There are a number of sub-classifications of Plugins along a number of different axes:
Slot - Where is the plugin displayed on the page?
Config - Which plugins should be loaded and when?
Discovery - How does the application learn which plugins should be loaded?
Hosting - Where is the plugin deployed?
Deployment - How is the plugin deployed?
All of these are broken down below into their various sub-classifications, which roughly correspond to increasing levels of effort.
A few key terms used in this document.
A website like edx.org. A second-level domain (SLD).
The platform (edx-platform et al) is deployed to a Site.
An application is a micro-frontend application deployed on the Site’s domain (or sub-domain). Applications are built by compiling static assets and deploying them to the web along with an index.html file to load them.
A Plugin is loaded in a Slot on the page. A slot is a particular mount point in the page (DOM) where the plugin should visually appear. Different slots will have different expectations for their dimensions. Some may grow vertically, horizontally, be statically sized, take over the page, exist in a modal, etc. A given slot may be configured to include multiple plugins. Plugin authors are responsible for understanding the requirements of the Slot in which they expect their plugin to appear, and those requirements will be communicated in documentation.
Below Sequence Nav
Plugin configuration is done once prior to usage of a plugin. It can be performed by course teams, developers, or operators. The configuration may be changed, of course, which will affect subsequent renderings of the plugin at runtime. Plugin configuration is primarily concerned with helping an Application determine which plugins should be loaded, where, when. It is not concerned with how those plugins choose to load their content.
Configuration as Code
This is effectively a hand-coded configuration document. It's easy to produce but error prone. Because it's code, it will generally need to be updated by a developer.
Configuration via UI
This is a plugin configuration that is created via a UI, serialized down into a configuration document like the above, and is handed to the application so it knows which plugins to load. The distinction here is that this is a level of effort above configuration-as-code. It's harder to produce, but easier and safer to use.
Allows plugins to be loaded under certain circumstances. For example, loading a plugin only on a specific course, or for particular classifications of users. Put another way, it implies letting the Application use its Context data (see below) in order to determine when the plugin should be loaded, not just where.
This is a heavy lift, as we expect that for practical purposes, it will be difficult to use without first implementing Configuration as UI.
Note that “context-sensitive” is different than being “context-aware”, which is described below in the Plugin Context section.
The process an Application follows to determine what plugins it should load.
This is taking a static or dynamic plugin configuration and handing it to the Application at build-time so that it "knows" it's plugin configuration immediately upon load. This potentially delivers a modest performance boost by reducing request overhead.
This is plugin discovery performed at run-time, as an Application is loading, via a separate call or configuration file download. It may be modestly slower than build-time plugin configuration because of the HTTP overhead of the extra download. It's also more flexible.
Information handed to the plugin after it has been loaded to help it understand how to display itself. This may include:
Page name / URL
Layout information (width, height)
Particular applications may have application-specific context they want to add in as well. In the case of courseware Applications, for instance, we’ll want to inform the plugin of:
To reduce back-and-forth, basic plugin context information can be supplied as query string parameters on the initial load request. This is what LTI does. However, to avoid running into predictable issues with URL length, application-specific context information should be transferred via Events (described below). We can think of this distinction as helping the plugin do progressive rendering; basic context information should be enough to orient the plugin as to where it’s being displayed, whereas application-specific context information is used to fill in content.
As a side note: We’ve been using the term “context aware plugins” - this is an unnecessary qualification, as all plugins are aware of whatever context we choose to share with them. The host page and our framework controls how much context to give plugins. It’s not clear that there are any use cases for providing different amounts of context to different plugins, however.
Plugins may communicate with the host page (and vice versa) via a pre-defined set of events. Each event has a type and an agreed-upon payload schema. These events will be delivered via the postMessage API. We may add to the set of pre-defined events as we discover new plugin use cases.
There will be a number of lifecycle events specifically to manage a plugin’s loading and rendering process. Because the host Application and the plugin can only communicate through the asynchronous postMessage API, we’ll need a set of events published by both sides to inform and properly sequence overall lifecycle state. As examples, we’ll likely need events related to: plugin loading, transferring context data, ready, content resize, and unloading.
If a plugin is deployed as part of the Site and is hosted under the Site’s domain, then it will have access to the Platform's authenticated APIs by virtue of having a valid JWT token.
If a plugin is hosted on a separate domain, then it will not have access to any Platform APIs. It will identify users and other resources via messages passed into it from the host page. If this is insufficient for it to implement its functionality, then it needs to become an Internal Plugin or an LTI Plugin. We may choose to implement a non-LTI authentication mechanism in the future, but for now our recommended path is LTI.
LTI (Sub-classification of External)
To allow externally hosted plugins to have access to authenticated Platform APIs, the plugin must conform to the LTI specification and be loaded in an iframe. This means the plugin must implement an OAuth/JWT authentication flow according to the LTI spec.
Plugins can be deployed in several different ways that affect their use cases.
Micro-frontend-based plugins have no backend. In that sense, they have limited ability to persist data. Deployments of the other types may make use of a micro-frontend for their frontend layer - this category is specifically for deployments that have no backend component.
Plugins that are served/associated with a stand-alone backend service, deployed independently from the Platform. If such a plugin has a micro-frontend layer, that is presumably still deployed separately.
Django App Plugin
A plugin that is deployed as part of the Platform via the Django App Plugin mechanism. If such a plugin has a micro-frontend layer, that is presumably still deployed separately.
A plugin based on an xBlock. Today, xBlocks were designed to be used in the actual course content, not in secondary/supporting functionality on the page. However, they may just work; some investigation will be required.
The LTI xBlock allows LTI tools to be loaded into an xBlock with the caveats described above. i.e., they can only be used on course content, not in supporting UIs.