When you need Moodle to do something it doesn't do out of the box, the instinct is to search the Moodle plugins directory. There are nearly 2,800 plugins listed there, surely someone has already solved your problem. And sometimes they have. But after building more than 300 custom plugins over fifteen years, we've seen the full cost of relying on community plugins for enterprise-grade requirements, and it's rarely as cheap as it first appears.
This isn't an argument against open-source community plugins. Many are excellent, and we use them ourselves. This is an argument for being deliberate about when custom development is the right call, and understanding what you're actually getting when you commission a plugin built specifically for your organization.
The Hidden Costs of Off-the-Shelf Plugins
Version Compatibility: The Upgrade Tax
Moodle releases a major version roughly every six months. Each release cycle, organizations running community plugins face what we've come to call the upgrade tax: the time and risk associated with checking whether every installed plugin has been updated for the new Moodle version, testing plugins that claim compatibility but behave differently against your data, and dealing with plugins whose maintainers have moved on.
We've audited Moodle installations for clients, KLM, Nedap, BSL Media & Learning, where between 10 and 30 percent of installed plugins were either unmaintained or running on versions two or three major releases behind. Those plugins don't just cause technical debt. They become the reason Moodle upgrades get postponed, which compounds into larger and more disruptive migrations later.
A custom plugin built to your organization's specifications is maintained by a party with a direct contractual interest in keeping it working. When Moodle 5.x introduces breaking API changes, we update our clients' plugins as part of the engagement, not as an afterthought if the original developer happens to be available.
Performance Bloat
Community plugins are built to serve many organizations with different needs. This means they carry configuration options you'll never use, database tables storing data you don't need, and event listeners firing on every page load regardless of whether that feature is relevant to your context.
We've profiled Moodle instances where a single community activity module was responsible for 30 to 40 percent of total database query time, because it was issuing broad queries optimized for flexibility rather than for the specific access pattern of the host organization.
When we build a custom activity module or report plugin, we design the schema around the actual queries it will run. Indexes are placed where they'll be used. Caching is implemented for the specific data that's expensive to compute. The result isn't just faster, it's predictably fast, because the plugin isn't doing anything it doesn't need to do.
Security Risks You Can't Control
The Moodle security team does excellent work, but they cannot review the entire plugin directory continuously. Community plugins vary enormously in their approach to security: some follow Moodle's security guidelines meticulously, others were written quickly to solve a specific problem and haven't been revisited since.
We've conducted more than 50 security audits of Moodle installations for Dutch organizations, and the findings follow a pattern. The core Moodle installation is usually in reasonable shape. The plugins are where the vulnerabilities accumulate, SQL injection through unsanitized parameters, cross-site scripting through unescaped output, access control checks that can be bypassed with a crafted URL.
With a custom plugin, you control the codebase. We apply Moodle's full security API, required_capability(), $DB->get_records() with parameterized queries, clean_param() on every external input, proper session key validation on form submissions. And because we wrote it, we know exactly where to look when a security review surfaces a concern.
The Support Gap
When a community plugin breaks during a Moodle upgrade, your options are limited. You can wait for the maintainer to release a fix. You can fork the plugin and fix it yourself, taking on maintenance responsibility. Or you can pay a developer, possibly us, to dig into a codebase we didn't write and don't know intimately.
None of these are good options when the plugin in question handles critical functionality: course completions feeding into HR systems, certification tracking for regulated industries, or the custom enrollment workflow that 15,000 users interact with every day.
What Custom Plugin Development Actually Looks Like
The process of building a custom Moodle plugin is more structured than people sometimes expect. It's not a matter of writing some PHP and dropping it into the plugins directory. Moodle has a well-defined plugin API, and working within it correctly is what makes plugins maintainable, upgradeable, and secure.
Phase 1: Requirements and Architecture
We start every plugin project with a requirements session where we push back on vague specifications. "We need a custom certificate plugin" is a starting point, not a spec. We need to know: what data goes on the certificate, who can generate it, what triggers issuance, where the PDFs are stored, how certificates are verified, what happens when a course is updated after a certificate is issued.
These conversations surface constraints that change the architecture. A certificate plugin for a healthcare training provider (like Dentallect) has different requirements than one for a corporate training platform, regulated professions may require certificates to reference specific qualification frameworks, include verifiable identifiers, and survive the organization's Moodle installation being replaced.
The output of this phase is a technical specification that identifies the plugin type (activity module, block, local plugin, auth plugin, format, report, admin tool), the database schema, the capability definitions, and the integration points with Moodle core.
Phase 2: Plugin Structure
Every Moodle plugin starts with a version.php that tells Moodle who made it, what version it is, and what version of Moodle it requires:
<?php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_clientactivity';
$plugin->version = 2026021500;
$plugin->requires = 2024042200; // Moodle 4.4
$plugin->maturity = MATURITY_STABLE;
$plugin->release = '1.2.0';The version number follows the date convention Moodle uses: YYYYMMDDXX. Every time we release an update, this number increments, which tells Moodle's upgrade system that database changes need to be applied.
For an activity module, lib.php defines the mandatory functions that Moodle calls to integrate the activity into the course flow:
<?php
function clientactivity_add_instance($moduleinstance, $mform = null): int {
global $DB;
$moduleinstance->timecreated = time();
$moduleinstance->timemodified = time();
return $DB->insert_record('clientactivity', $moduleinstance);
}
function clientactivity_supports(int $feature): ?bool {
return match ($feature) {
FEATURE_MOD_INTRO => true,
FEATURE_COMPLETION_TRACKS_VIEWS => true,
FEATURE_GRADE_HAS_GRADE => true,
FEATURE_BACKUP_MOODLE2 => true,
default => null,
};
}The clientactivity_supports() function is what tells Moodle which platform features this activity works with, completion tracking, grading, backup/restore. Getting this right matters: if you claim FEATURE_BACKUP_MOODLE2 support without implementing the backup API properly, courses containing your activity will produce corrupt backup files.
Phase 3: Database and Upgrades
The db/install.xml file defines the initial schema using Moodle's XMLDB format. When the plugin is first installed, Moodle's upgrade system creates these tables automatically. When we release an update that changes the schema, we add a step to db/upgrade.php:
<?php
function xmldb_clientactivity_upgrade(int $oldversion): bool {
global $DB;
$dbman = $DB->get_manager();
if ($oldversion < 2026021500) {
$table = new xmldb_table('clientactivity');
$field = new xmldb_field('externalref', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timemodified');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
upgrade_mod_savepoint(true, 2026021500, 'clientactivity');
}
return true;
}This is how schema changes survive upgrades without data loss. The version check ensures each migration only runs once, even if an administrator runs the upgrade process multiple times.
Phase 4: Testing
We test plugins at multiple levels depending on the project's scope and budget. Sometimes clients request unit tests, where PHP classes that implement the plugin's core functionality are tested with PHPUnit, with Moodle's test data generator providing realistic fixture data. In other cases the budget is limited or the plugin is straightforward enough that unit tests don't add proportional value, so we focus testing effort where it matters most. Integration tests run against a real Moodle database to verify that the plugin installs cleanly, that the database schema is created correctly, and that core Moodle APIs interact with the plugin as expected. And before any plugin goes to production, we do manual acceptance testing against a staging instance that mirrors the client's production configuration.
We also run Moodle's built-in code checker tools: phpcbf for code style, phplint for syntax errors, and Moodle's own moodlecheck plugin for API compliance. The community plugins directory requires these checks to pass, but for custom plugins, we apply them because they catch real problems.
All of this is tied together with CI pipelines. Every commit triggers automated checks: code style, linting, and any configured tests run automatically before code can be merged. For deployment, we use CI/CD pipelines that handle staging and production releases, so updates reach the right environment without manual intervention or human error.
Types of Plugins We Build
Activity Modules
The most complex plugin type. Activity modules appear in the course section list alongside Moodle's built-in activities (Assignment, Quiz, Forum). Real examples from our portfolio include:
- KLM Travel Journal for cabin crew training, where crew members document real-world experiences as structured learning activities
- Open Webinar for live and recorded webinar sessions with attendance tracking and completion integration
- Custom certificates with automated generation based on course completion and configurable templates
- Self-assessment modules and 360-degree feedback tools for professional development programs
- Time registration activities for tracking study hours in regulated training environments
- Canvas-based interactive activities for creative and visual learning assignments
Authentication and Enrollment Plugins
Auth plugins control how users authenticate. Enrol plugins control how users get into courses. These two areas are where enterprise system integration happens.
We have built dozens of authentication integrations for organizations like KLM, AFAS, SURFconext, Fontys, and UWV. These range from SAML2 and OpenID Connect (OIDC) implementations to fully custom SSO solutions. Each handles organization-specific attribute mapping: taking attributes from the identity provider and using them to populate Moodle profile fields, assign cohorts, or trigger enrollment in specific course categories. For HR-system integrations (SAP, AFAS, Workday), we have built enrollment sync plugins that poll an API on a schedule, create users who have arrived, suspend users who have left, and update profile data in between. We also build enrollment plugins for payment integrations (Mollie, course payment gateways) and voucher-based access.
Reports and Analytics
Moodle's built-in reporting is adequate for simple use cases. For enterprise clients who need to answer questions like "which of our 8,000 employees completed the mandatory safety training this quarter, broken down by department and employment type," the built-in tools reach their limits quickly.
We build report plugins using Moodle's report API, which integrates them into the administration and course-level navigation. Examples include certificate reports, coach view completion dashboards, group progress overviews, and SCORM attempt analytics. These reports can join completion data with profile fields, custom user info fields, and cohort membership to produce the exact views that HR and compliance teams need.
Course Formats
Course formats control how a course's section list is presented to students. Beyond the built-in Topics, Weeks, and Social formats, we have built:
- VSF (Vertical Section Format) for clean, scrollable course layouts
- Tab-based formats like TabTiles for VUmc, organizing sections into navigable tabs
- Grid formats customized for KLM, UWV, and other enterprise clients
- ANWB Streetwise format for primary school traffic safety education
- Game dashboard formats with interactive progression elements
Blocks
Blocks are sidebar components that can be added to any page in Moodle. We use them for dashboard widgets, notification systems, quick-access menus, and embedding content from external systems. Real examples include voucher management blocks, recertification tracking, user avatar (webcam snapshot) blocks, course note widgets, Streetwise navigation for ANWB, and custom dashboard blocks for various enterprise clients. We have also built Commander, a quick-navigation block that lets administrators and teachers find pages instantly.
Themes
Moodle's theme system controls the entire front-end presentation. For enterprise clients, this usually means a custom theme built on Moodle's Boost base theme, implementing the organization's design system, adjusting navigation structure, and ensuring accessibility compliance. We have built themes for Nedap, De Schoolschrijver, Vakmedia, Citaverde, and others, each matching strict brand guidelines.
Availability Conditions and Local Plugins
Beyond the main plugin types, we build availability conditions that control access to activities based on criteria like IP address restrictions, voucher codes, or payment status. Our local plugins handle everything from GDPR compliance tools and OneRoster data synchronization to CSV user imports and quiz monitoring.
The Cost-Benefit Analysis: When Does Custom Make Sense?
Community plugins make sense when the functionality they provide closely matches what you need, when they're actively maintained, and when their security posture is acceptable for your environment.
Custom development makes sense when:
- The community plugin exists but doesn't fit. If you need to modify a community plugin significantly, you've effectively forked it, and now you're responsible for maintaining a diverged codebase through every Moodle upgrade. It's often cheaper to build what you actually need.
- The functionality involves sensitive data or access control. Enrollment logic, grade calculations, and certificate issuance are areas where a bug has real consequences. These are not places to accept code you can't fully audit.
- The plugin needs to integrate with your systems. Community plugins are built for a general audience. A plugin that syncs with your specific HR system, using your specific API authentication scheme and your specific data model, needs to be written for your situation.
- You're operating at scale. A plugin that performs acceptably for 500 users may not perform acceptably for 50,000. Custom plugins can be optimized for your specific load profile and access patterns.
- You need guaranteed support continuity. For functionality that's critical to your operations, you need to know that someone with intimate knowledge of the code is available when something goes wrong.
How to Evaluate Whether You Need a Custom Plugin
Before commissioning custom development, work through these questions:
Does a community plugin exist that covers 80% or more of your requirements without modification?
If yes, evaluate whether the remaining 20% is truly necessary or whether you can adapt your process.
Is the community plugin actively maintained?
Check the last update date, the number of open issues, and whether the maintainer has responded to recent bug reports. A plugin last updated for Moodle 3.9 is effectively unmaintained.
Have you read the community plugin's code?
For any plugin handling authentication, enrollment, grades, or financial transactions, you should review the code or have someone review it for you. The Moodle plugin directory does not guarantee security.
What's the cost of the plugin failing?
If the answer is "we lose compliance records" or "enrollment for 10,000 users breaks," the calculus shifts toward custom development with guaranteed maintenance.
Does the plugin need to survive your organization across multiple Moodle versions?
Longevity requirements favor custom development with a maintenance agreement.
Realistic Expectations
Custom plugin development takes longer and costs more upfront than installing a community plugin. That's a real difference, and it matters for organizations with tight timelines or limited budgets.
What it buys you is a plugin that does exactly what you need, performs well at your scale, can be audited and maintained by people who know the code, and won't hold up your Moodle upgrade because its maintainer disappeared.
For Ldesign Media's clients, organizations like KLM running training for thousands of employees, or Yuverta managing student progression across a network of MBO schools, that reliability is the point. The training platform is infrastructure, and infrastructure needs to be dependable.
The 300+ plugins we've built since 2010 range from small utility plugins that solve one specific problem, to complex multi-component systems that sit at the center of a client's entire learning operation. The common thread isn't complexity, it's that in every case, the plugin does what the organization actually needs, and it keeps working when Moodle updates.



