Driving Continuous Improvement at Scale

Driving continuous improvement at scale. photo of David Taylor, Developer Experience Tech Lead

Back in 2013, Discourse launched with Rails 3 and Ember 1.0. Over the years, we’ve evolved alongside those frameworks and our many other dependencies - taking full advantage of new features, performance improvements, and modern best practices as they’ve arrived.

For more than a decade, we’ve never needed a full rewrite. Every major upgrade has been adopted incrementally, and our ecosystem of themes and plugins has come along for the ride. This incremental mindset is one of the most important ingredients in keeping Discourse sustainable at scale.

In recent years, we’ve formalized the mindset with a dedicated Developer Experience Team, whose mission is to keep the development experience across the Discourse ecosystem as smooth as possible. A core part of that work is staying current with framework releases, and building workflows & tooling to make upgrades predictable, reliable and fast.

Sounds great! But how do we actually do it?

A Quick Tour of Discourse Extensibility

Discourse can be extended through themes and plugins. At a high level:

  • Themes customize the frontend
  • Plugins can customize both frontend and backend

Frontend and backend customizations can access the full power of Rails and Ember, which gives developers immense flexibility, and an equally large responsibility to keep their code updated as best practices evolve. This balance of openness and responsibility is core to our ethos as an open-source construction kit for civilized discussion.

Driving incremental improvements

Whenever we introduce new APIs or patterns in Discourse, our goal is for new and old approaches to coexist. That allows us to migrate code gradually: first in Discourse core, then in official themes and plugins, before finally deprecating and removing old patterns.

For our broader ecosystem, this means plugin and theme authors can update at their own pace, without needing to coordinate around a single “big bang” release.

Most Discourse customizations touch the frontend, so the careful rollout of changes there is especially critical. Ember’s approach to versioning has been invaluable: new APIs land without breaking existing apps, and deprecated APIs remain interoperable for a time. That philosophy aligns perfectly with our own workflows for driving change.

We move platform changes through four deliberate phases:

  1. Deprecation introduced, but silenced in most environments. At this early stage, we don’t want to “cry wolf” and fill all developer’s consoles with warning messages. Engineers working on the upgrade project will enable the messages in their local environments, and work to modernize relevant code in Discourse core and official themes/plugins.
  2. Deprecation unsilenced, shown in the browser developer console, and surfaced in theme/plugin test suites.
  3. Escalation to admin warning banners, as a final reminder for any remaining usage in production.
  4. Deprecated API removed.
Final warning when a deprecation is unresolved
Final warning when a deprecation is unresolved

Throughout this process, we provide tooling for hosting providers to collect real‑world telemetry via the discourse-deprecation-collector. This helps us to adjust timelines, improve documentation, and identify where customers may need assistance.

Production telemetry graph showing a deprecation being resolved
Production telemetry showing a deprecation being resolved

Automating upgrade work

We maintain over 500 themes and plugins (more than half of which are open source). Scaling this work requires both automation and trust in our tooling.

Our first line of defense is always linting. We ship custom lint rules (often with autofixers) to detect and modernize deprecated patterns. We use ESLint on the frontend, and RuboCop for the backend. Sometimes upstream libraries like eslint-plugin-ember include the rules we need, but when they don’t, we build and distribute our own Discourse-specific rules.

At first glance, writing a custom lint rule might look like more effort than just fixing the code manually. But across hundreds of repositories, the investment pays off fast. Once a rule has been refined and tested, we can apply it across our entire ecosystem with high confidence and minimal risk.

For more extensive changes which can’t be handled by static analysis on individual files, we also make use of codemods. For example, our discourse-gjs-codemod, which is a lightly-customized version of the Ember team’s template-tag-codemod.

Screenshot of an announcement and documentation for the discourse-gjs-codemod
Announcement and documentation for the discourse-gjs-codemod

To apply codemods and lint fixes across 500+ repositories, we use mass-pr. This tool helps us iterate through hundreds of repositories, apply upgrades, and create pull requests for the changes. It also supports pausing for human intervention when needed.

For mechanical, low‑risk changes we then safely merge at scale using our mass-merge tool.

Looking ahead

Continuous, incremental modernization has allowed Discourse to evolve without disruptive rewrites - keeping quality high, APIs stable, and our team productive.

The tools and workflows we’ve built turn large upgrade projects into repeatable, well-understood processes that can happen alongside everyday feature development. They’ve also made it easier for our open-source ecosystem to keep their customizations healthy and compatible.

As frameworks and best practices continue to evolve, our commitment to incremental improvement remains the same. It’s not always glamorous work - but it’s what keeps Discourse moving forward.