Discourse Onboarding LabChapter VII — Plugin System: Extending Core at Boot0/6Contents
Chapter VII

Plugin System: Extending Core at Boot

Discourse plugins are not compiled into core — each plugin ships a plugin.rb that gets instance_eval'd against a Plugin::Instance at boot, giving it a DSL (register_asset, after_initialize, reloadable_patch, add_to_class, etc.) for wiring in assets, routes, and callbacks. The poll plugin shows the sharpest case of this: it uses reloadable_patch to Post.prepend(DiscoursePoll::PostExtension), permanently splicing has_many :polls and an after_save hook into the 'core' Post model. New engineers should internalize that grep-ing app/models/post.rb alone will miss behavior that actually originates from plugin code executed at boot.


How a plugin.rb file becomes running code
Discourse.activate_plugins!lib/discourse.rb
Plugin::Instance.find_alllib/plugin/instance.rb
activate! (instance_eval)lib/plugin/instance.rb
after_initialize block runslib/plugin/instance.rb
reloadable_patch { Post.prepend(...) }plugins/poll/plugin.rb
Post gains poll behaviorplugins/poll/lib/post_extension.rb
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
walkthrough

plugin.rb is a script run against a Plugin::Instance, not a normal Ruby file

plugins/poll/plugin.rb · lines 124
lines 3–7

These aren't ordinary comments — Plugin::Metadata.parse_line reads '# name:', '# about:' etc. line-by-line before the plugin is ever activated, populating the metadata shown in the admin plugin list.

lines 9–12

register_asset looks like a top-level function call, but because activate! runs plugin.rb via instance_eval, this is actually plugin_instance.register_asset(...) — it queues the file to be registered as a CSS asset later.

line 16

enabled_site_setting wires poll's on/off switch to the poll_enabled site setting; every enabled?-guarded method in Plugin::Instance checks this before running plugin logic.

line 24

after_initialize doesn't run its block immediately — it stores it, and notify_after_initialize calls every stored block only once all plugins have finished their top-level activation. This is where poll mounts its engine and monkey-patches Post.

# frozen_string_literal: true# name: poll# about: Official poll plugin for Discourse# version: 1.0# authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip)# url: https://github.com/discourse/discourse/tree/main/plugins/pollregister_asset "stylesheets/common/poll.scss"register_asset "stylesheets/common/poll-ui-builder.scss"register_asset "stylesheets/desktop/poll-ui-builder.scss", :desktopregister_asset "stylesheets/common/poll-breakdown.scss"register_svg_icon "far-square-check"enabled_site_setting :poll_enabledmodule ::DiscoursePoll  PLUGIN_NAME = "poll"endrequire_relative "lib/poll/engine"after_initialize do
step 1 of 4
  reloadable_patch do    Post.prepend(DiscoursePoll::PostExtension)    User.prepend(DiscoursePoll::UserExtension)  end
plugins/poll/plugin.rb · lines 4649
Figure II · This is the literal monkey-patch: at boot, Post (a core ActiveRecord model defined in app/models/post.rb) has DiscoursePoll::PostExtension prepended onto its ancestor chain, so poll-specific methods and associations become indistinguishable from 'native' Post behavior.
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 2 passed

How does a bare top-level call like `register_asset "..."` in plugin.rb actually get executed?

Why does poll wrap `Post.prepend(DiscoursePoll::PostExtension)` inside `reloadable_patch` instead of calling prepend directly at the top level?