Skip to content

Open edX — Development Paths for New Features

Purpose: This document helps a developer (or an LLM guiding a developer) choose the right technical approach for building new functionality on Open edX. For each path, it explains what it is, when to use it, when not to use it, and provides detailed real-world examples with rationale.


Quick Decision Guide

Answer these questions in order to find the right path:

1. Does the feature live INSIDE a course as a learning component?
   └─ YES → XBlock

2. Does the feature need to intercept or modify a core platform event
   (enrollment, login, grading, certificate) WITHOUT adding a UI?
   └─ YES → Hooks & Filters (inside a Django Plugin)

3. Does the feature need new backend logic, new data models, or new REST APIs?
   └─ YES → Django Plugin

4. Does the feature need a user interface (a page, a dashboard, a flow)?
   └─ YES → MFE
   └─ AND requires new data/logic → Django Plugin + MFE (combined)

Most non-trivial features use Django Plugin + MFE together. The plugin owns the data and logic; the MFE owns the UI.


Path 1 — Micro-Frontend (MFE)

What it is

An MFE is a standalone React application that runs in the browser and communicates with edx-platform exclusively through REST APIs. It lives in its own repository, has its own build and deployment pipeline, and replaces or extends the HTML pages that edx-platform used to render server-side.

Architecture

Browser
  └── MFE (React, your repo)
        └── calls REST APIs
              └── edx-platform (Django) — data layer, auth, business logic

The MFE is served as static files (HTML/JS/CSS). It does not modify edx-platform itself. Authentication is handled via JWT cookies issued by the platform.

When to use it

  • You need a new page or user experience that does not exist in the platform today
  • You need to replace or heavily customize an existing page (e.g., dashboard, course player, registration)
  • The feature is primarily a UI concern — the data already exists in the platform APIs
  • You want the frontend to be deployed and updated independently from the platform backend

When NOT to use it

  • The feature is only backend logic with no user interface → use Django Plugin instead
  • The feature is a learning component inside a course (video, quiz, simulation) → use XBlock instead
  • You only need to change the look of an existing page slightly → use Paragon theming or MFE config overrides, not a full custom MFE
  • You need to intercept platform events (enrollment, grading) → use Hooks & Filters instead

Real examples

Example 1: Custom learner dashboard

An operator wanted a learner dashboard that showed enrolled courses grouped by program, with a progress ring per program and direct links to certificates. The default frontend-app-dashboard showed a flat list of courses with no program grouping.

  • Approach: Forked frontend-app-dashboard, added program grouping logic in the React components, consumed the existing /api/enrollment/v1/enrollment and /api/program/v1/programs REST endpoints already available in edx-platform.
  • Why MFE and not a Django modification: The data already existed in the platform APIs. The only missing piece was the UI logic to group and display it. No new backend code was needed.
  • Why not XBlock: The dashboard is not inside a course — it is a platform-level page.

Example 2: Custom authentication flow (frontend-app-authn replacement)

A corporate training platform needed a single sign-on experience where users could only register if their email domain matched a company whitelist, and the registration form had custom fields (employee ID, department).

  • Approach: Built a custom MFE that replaced frontend-app-authn. The MFE handled form rendering and validation. A Django Plugin added the domain whitelist check and custom profile fields to the backend.
  • Why MFE: The registration UI needed to be completely different from the default Open edX flow.
  • Why also a Django Plugin: The domain whitelist logic and custom profile field storage required new backend code that could not live in the MFE.

Example 3: Gradebook with custom weighting rules

An institution had complex grade weighting rules (labs count 40%, midterm 30%, final 30%) that the standard gradebook did not support. They needed instructors to configure weights and see the resulting grades.

  • Approach: Extended frontend-app-gradebook with a weight configuration panel. A Django Plugin added a new REST endpoint to store and apply the custom weights.
  • Why MFE: The gradebook is an existing MFE — extending it was more efficient than building from scratch.

Path 2 — Django Plugin

What it is

A Django Plugin is a standard Python/Django application that lives in its own repository and is installed into edx-platform as a pip dependency. The platform automatically discovers and loads it using an entry point mechanism (stevedore). The plugin can add new Django models, REST API endpoints, admin pages, management commands, and Django signal handlers — all without modifying a single file in edx-platform.

Architecture

edx-platform (Django)
  └── discovers plugins via entry points (stevedore)
        └── your-plugin (installed via pip)
              ├── models.py        — new database tables
              ├── views.py         — new REST endpoints
              ├── urls.py          — URL routing
              ├── signals.py       — react to platform events
              └── apps.py          — plugin_app config (required)

The key in apps.py:

python
class YourPluginConfig(AppConfig):
    plugin_app = {
        'url_config': {
            'lms.djangoapp': {
                'namespace': 'your-plugin',
                'regex': r'^api/your-plugin/',
                'relative_path': 'urls',
            }
        },
        'settings_config': {
            'lms.djangoapp': {
                'common': {'relative_path': 'settings.common'},
            }
        }
    }

This tells edx-platform to mount your URLs and settings automatically.

When to use it

  • You need new database models to store data that the platform does not have
  • You need new REST API endpoints for your MFE or external systems to consume
  • You need to react to platform events (user enrolled, grade changed, certificate issued) and run your own logic
  • You need admin panel pages for operators to manage your feature's data
  • You need to add Django management commands (batch jobs, data migrations)
  • You need to add logic that runs server-side for security, performance, or data integrity reasons

When NOT to use it

  • The feature is only a UI change with no new data or logic → use MFE only
  • The feature is a learning component inside a course → use XBlock instead
  • You need to intercept and modify the platform's own internal flow (not just react to it) → use Hooks & Filters (which live inside a plugin, but the mechanism is different)
  • You are tempted to modify edx-platform directly → always use a plugin instead; direct modifications are overwritten on every platform upgrade

Real examples

Example 1: platform-plugin-aspects (official Open edX plugin)

The Aspects analytics system adds learning analytics capabilities to Open edX. It needed to capture learning events, store them in ClickHouse (an analytics database), and expose aggregated data via REST APIs.

  • What the plugin adds: Django signal handlers that listen to xAPI events emitted by the platform, a Celery task pipeline to forward events to ClickHouse, and REST endpoints for the analytics dashboard MFE to consume.
  • Why Django Plugin: The feature required new data pipelines and storage that did not exist in edx-platform. There was no way to implement this without backend code.
  • Why not just an MFE: The data collection and processing happens server-side. An MFE can only display data, not collect it.

Example 2: platform-plugin-ontask (eduNEXT)

OnTask is a learning analytics tool that connects to Open edX to personalize learning by sending targeted messages to students based on their activity. The plugin hooks into the platform's instructor dashboard.

  • What the plugin adds: A new tab in the LMS instructor dashboard (via Django template extension), API endpoints that OnTask calls to fetch student activity data, and configuration settings for connecting to the OnTask server.
  • Why Django Plugin: The integration required adding UI to an existing Django-rendered page (instructor dashboard), not a standalone new page. It also needed backend endpoints that OnTask's external server would call.
  • Why not an MFE: The instructor dashboard was still Django-rendered at the time of development. The feature was an extension to an existing backend-rendered page.

Example 3: Custom enrollment validation

A platform needed to prevent students from enrolling in a course if they had not completed a prerequisite course with a grade above 70%.

  • What the plugin adds: A Django signal handler on ENROLL_STATUS_CHANGE, a REST endpoint to check prerequisite completion status, and admin configuration to map prerequisite relationships between courses.
  • Why Django Plugin: Enrollment validation must happen server-side. A client-side check in an MFE is not secure — users can bypass it.
  • Why not Hooks & Filters: Actually, this is a good candidate for the Filters mechanism (see Path 3 below). In practice, the developer used a signal handler because they needed to run the check after enrollment, not block it before.

Path 3 — Hooks & Filters (Open edX Filters)

What it is

The Hooks Extension Framework (OEP-50) is a mechanism built into edx-platform that allows plugins to intercept and modify the platform's internal execution flow at specific, well-defined points — without monkey-patching or modifying platform code.

There are two types:

  • Filters: Intercept an action before it completes. Can modify data or halt the action entirely (e.g., block enrollment, change certificate data before it is generated).
  • Events (Signals): Notify plugins after an action has completed. Cannot modify the outcome — only react to it.

Filters always live inside a Django Plugin. They are not a standalone path — they are a capability you add to a plugin.

Architecture

edx-platform internal flow:
  └── "student tries to enroll"
        └── platform calls StudentEnrollmentRequested filter
              └── your plugin's pipeline step runs
                    ├── reads enrollment data
                    ├── optionally modifies it
                    └── either returns data (allow) or raises exception (block)
        └── enrollment proceeds (or is blocked)

When to use it

  • You need to block or modify a core platform action before it completes: enrollment, login, certificate generation, grading, course access
  • You need to add data to an existing platform object before it is saved (e.g., add custom fields to a certificate)
  • You need to enforce business rules that must run inside the platform's own flow, not after the fact
  • You need behavior that is synchronous — the calling code needs a result before continuing

When NOT to use it

  • You only need to react after an action — use Django signals instead (simpler)
  • The filter point you need does not exist yet in openedx-filters — you would need to propose a new one to the community, which takes time
  • You need to add a completely new feature, not modify an existing flow — use a Django Plugin with models and views instead

Available filter points (examples)

FilterWhen it firesWhat you can do
StudentEnrollmentRequestedBefore a student enrolls in a courseBlock enrollment, modify enrollment data
CourseEnrollmentStartedAfter enrollment beginsReact, cannot block
StudentLoginRequestedBefore login completesBlock login (e.g., account suspended)
CertificateCreationRequestedBefore a certificate is generatedModify certificate data, block issuance
CourseGradeCalculatedAfter grade is calculatedReact to grade, send to external system
CourseAboutRenderStartedBefore course about page rendersModify page context

Real examples

Example 1: Blocking enrollment based on subscription tier

A SaaS platform had subscription tiers. Free users could only enroll in up to 3 courses. Premium users had unlimited enrollment.

  • What the filter does: Implements a pipeline step on StudentEnrollmentRequested. The step checks the user's subscription tier against the number of active enrollments. If the limit is exceeded, it raises PreventEnrollment, which the platform catches and surfaces as an error to the user.
  • Why Filters and not a signal: A signal fires after enrollment — the student would already be enrolled by the time the signal fires. The filter fires before, which is the only way to prevent the action.
  • Why not an MFE check: Client-side checks can be bypassed via the API. The enrollment API must enforce the rule server-side.

Example 2: Customizing certificates with company logo

A corporate training operator issued certificates that needed to include the employer company's logo, not the generic Open edX logo. Each user was associated with a company in a custom profile field.

  • What the filter does: Implements a pipeline step on CertificateCreationRequested. The step reads the user's company profile field, fetches the company logo URL, and adds it to the certificate context before generation.
  • Why Filters: The certificate generation happens inside the platform's rendering pipeline. The only way to inject data into the certificate template is at the filter point before rendering.
  • Why not a Django model override: The certificate template and generation logic is deep inside edx-platform. Modifying it directly would break on every platform upgrade.

Path 4 — XBlock

What it is

An XBlock is a Python component that defines a new type of learning content that can be placed inside a course in Studio. It has a backend (Python, for data storage and grading) and a frontend (HTML/CSS/JavaScript, for the student interface). XBlocks are the correct way to add new interactive learning activities to Open edX courses.

Architecture

Course in Studio
  └── Unit
        └── XBlock instance (your component)
              ├── student_view()     — HTML rendered to the learner
              ├── studio_view()      — HTML rendered in Studio for configuration
              ├── handle_ajax()      — handles student interactions (button clicks, submissions)
              └── fields             — data stored per student, per course

When to use it

  • You need a new type of learning activity inside a course: a custom quiz type, a simulation, a coding exercise, a flashcard deck, a collaborative tool
  • The component needs to store per-student data (answers, progress, time spent)
  • The component needs to report a grade back to the gradebook
  • The component needs a Studio configuration interface for course authors
  • The activity is reusable across multiple courses on the same platform

When NOT to use it

  • The feature is a platform-level page (dashboard, profile, reports) → use MFE instead
  • The feature is backend logic with no course-level component → use Django Plugin instead
  • You only need to embed an external tool (a third-party website or simulation) → use the built-in LTI XBlock instead of writing a custom one
  • The interactivity is simple enough to be handled with JavaScript in an HTML component → consider the built-in HTML/Problem components first

Real examples

Example 1: AI-powered short answer XBlock (OpenCraft)

OpenCraft built an XBlock where students write a short text response and receive immediate feedback from an LLM based on a rubric set by the course author.

  • What the XBlock does: Renders a text area and submit button (student_view). On submission, calls an LLM API with the student's response and the author-defined rubric, then displays the feedback. Stores the submission and feedback per student. Optionally reports a grade if the author enables grading.
  • Why XBlock: The component lives inside a course unit, stores per-student data, and integrates with the gradebook. All of these requirements are exactly what the XBlock architecture is designed for.
  • Why not a Django Plugin: A plugin cannot render content inside a course unit or interact with the gradebook at the unit level.
  • Why not an MFE: An MFE is a standalone page, not a component embedded inside a course.

Example 2: Interactive flashcard XBlock (OpenCraft)

A flashcard component where course authors upload up to 8 cards (text or image, front and back), and students flip through them with animation and optional audio.

  • What the XBlock does: Renders the flashcard deck UI (student_view). Stores which cards the student has reviewed per session. Studio view allows the author to add, edit, and reorder cards.
  • Why XBlock: Flashcards are a learning component — they belong inside a course unit. The component needs a Studio interface for authors to configure it, and it needs to store student interaction data.
  • Why not a static HTML component: The flip animation, audio, and per-student progress tracking require a proper XBlock with JavaScript handlers and field storage.

Example 3: Oppia interactive activities XBlock

Oppia is an external interactive learning platform. The Oppia XBlock embeds Oppia explorations (adaptive, branching learning activities) directly inside Open edX courses, and forwards the learning events Oppia emits into the Open edX analytics infrastructure.

  • What the XBlock does: Renders an Oppia exploration in an iframe inside the course unit. Listens to events emitted by the Oppia iframe and forwards them to edx-platform's event tracking system.
  • Why XBlock: The integration point is inside a course unit. The XBlock is the only way to embed a component at that level and hook into the analytics pipeline simultaneously.
  • Why not just an iframe in an HTML component: A raw iframe cannot communicate with edx-platform's event system. The XBlock's JavaScript handler layer is needed to bridge the two.

Combined Paths — Common Patterns

Most significant features require more than one path. These are the most common combinations:

Django Plugin + MFE

The most common pattern for non-trivial features. The plugin owns data storage and business logic; the MFE owns the user interface.

Example: A certificate verification portal. The Django Plugin adds a REST endpoint that verifies a certificate UUID and returns its metadata. The MFE provides the public-facing verification page with the certificate details.


Django Plugin + Hooks & Filters

Used when a feature needs both its own data (models, APIs) and the ability to intercept platform events.

Example: A prerequisite enforcement system. The plugin stores prerequisite relationships in its own models and exposes an admin UI to configure them. A Filter pipeline step uses that data to block enrollment if prerequisites are not met.


Django Plugin + XBlock

Used when a learning component (XBlock) needs to share data with platform-level features or needs a more complex backend than a simple XBlock allows.

Example: An adaptive learning system. The XBlock renders adaptive exercises inside the course. The Django Plugin stores the student's learning profile across courses and exposes an API the XBlock calls to determine which exercise to show next.


What you cannot (should not) do

ApproachWhy to avoid it
Modifying edx-platform directlyChanges are overwritten on every platform upgrade (every 6 months). Unsustainable.
Putting business logic inside an MFEClient-side logic is not trustworthy for security-sensitive operations. APIs can be called directly, bypassing MFE checks.
Using XBlocks for platform-level featuresXBlocks can only run inside course units. They have no access to platform-level pages or navigation.
Using a Django Plugin to render full pagesDjango-rendered pages are being deprecated in favor of MFEs. New pages should be MFEs.

Sources:

Schema Education — Internal Research