Transform Learning with a Powerful Enterprise LMS

Learn more
WordPress LMS

Tutor LMS Architecture: An Overview for Developers

How Tutor LMS 4.x boots, which objects you call, where courses, enrolments and progress are stored, the hooks integrations need, the 4.0 learning area and how Pro loads on top of core.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Layered diagram of the Tutor LMS 4.x architecture: core singleton, config and utility objects, data tables and Tutor LMS Pro add-ons

Tutor LMS architecture comes down to one singleton, one config object and one large utility class. When WordPress loads the plugin file, tutor.php builds the TUTOR\Tutor instance, which registers the custom tables on $wpdb, instantiates roughly 45 components in one constructor and then fires tutor_loaded. Everything else you touch as a developer hangs off that: tutor() for configuration, tutor_utils() for helper and model calls, an action router for form posts, a template loader with theme overrides, and a key-authenticated REST namespace.

This overview is for developers who need a mental map before they write an integration, debug a conflict or plan a migration. It covers the load sequence, the main objects, how data is split between WordPress tables and custom tables, and where Tutor LMS Pro plugs in. For step-by-step site building, use the guides on Tutor LMS Expert.

Checked against the source of Tutor LMS 4.1.3 and Tutor LMS Pro 4.1.3 (October 2026). File and line references below are from that release; confirm them against the version you run before you rely on a line number.

The load sequence: from tutor.php to tutor_loaded

The bootstrap is short and runs at plugin-include time, not on a WordPress hook:

  1. tutor.php requires the Composer autoloader, defines TUTOR_VERSION, TUTOR_FILE and TUTOR_ENV, and registers the activation, deactivation and uninstall callbacks.
  2. It then runs $GLOBALS['tutor'] = tutor_lms();, and tutor_lms() returns Tutor::get_instance().
  3. The TUTOR\Tutor constructor (classes/Tutor.php) adds $wpdb aliases for 17 tables, includes the function files, and fires tutor_before_load.
  4. It boots the GDPR module and instantiates the components directly: Addons, Post_types, Assets, Admin, Ajax, Options_V2, Shortcode, Course, Lesson, Template, Quiz, WooCommerce, TutorEDD, RestAuth, QuizBuilder, Ecommerce, Migration and more. Each component registers its own hooks in its constructor.
  5. run() fires tutor_before_run and tutor_after_run, and the constructor then fires tutor_loaded.
  6. Only after that does it attach init_action() and update_instructor_capability() to init.

The practical consequence: tutor_loaded fires while the Tutor plugin file is being included. A plugin that WordPress includes earlier can hook it; a plugin included later has missed it. For third-party code, hooking plugins_loaded and checking that tutor() exists is the dependable pattern.

<?php
// Not run on a live site; test on staging first.
add_action( 'plugins_loaded', 'acme_tutor_bootstrap' );

function acme_tutor_bootstrap() {
    if ( ! function_exists( 'tutor' ) ) {
        return; // Tutor LMS is not active.
    }
    add_action( 'tutor_after_enrolled', 'acme_on_tutor_enrolled', 10, 3 );
}

function acme_on_tutor_enrolled( $course_id, $user_id, $enrolled_id ) {
    // Push the enrolment to a CRM queue, an LRS, etc.
}

The core objects: tutor(), tutor_utils() and the __call bridge

Three global helpers cover most developer access.

HelperReturnsUse it for
tutor_lms() / $GLOBALS['tutor']The TUTOR\Tutor singletonReaching component instances held as properties
tutor()TUTOR\ConfigPaths, URL, version, nonce names, has_pro and every post type name
tutor_utils() (alias tutils())One cached TUTOR\Utils instanceOptions, enrolment checks, course lookups, dashboard menus

tutor() exposes its settings as properties through __get, and each post type name passes through a filter: tutor()->course_post_type is courses by default (filter tutor_course_post_type), tutor()->lesson_post_type is lesson, tutor()->quiz_post_type is tutor_quiz and tutor()->enrollment_post_type is tutor_enrolled. Read the names from tutor() rather than hard-coding them, especially courses, which replaced the pre-1.3.1 course.

TUTOR\Utils is over 10,000 lines. Since 2.0.6, much of its logic lives in model classes under Tutor\Models, and Utils::__call() forwards unknown method calls to CourseModel, LessonModel, QuizModel, WithdrawModel and EnrollmentModel. That keeps legacy calls such as tutor_utils()->do_enroll() working. It also means a mistyped method name silently returns null instead of throwing an error, so call the model class directly in new code, for example Tutor\Models\EnrollmentModel.

Request handling: actions, AJAX and REST

Tutor accepts input through four channels, and knowing which one a feature uses tells you where to hook.

  • Form-post router. On init, Tutor::init_action() reads the tutor_action request field and fires tutor_action_{$tutor_action}. Login, password reset and the native "pay now" flow (tutor_action_tutor_pay_now) use this.
  • AJAX. Core registers 137 unique wp_ajax_* actions, 8 of them also available to logged-out users. The React course builder saves through actions such as tutor_create_course, tutor_update_course and tutor_save_topic. Nonces use action tutor_nonce_action and field _tutor_nonce. Tutor also overrides wp_doing_ajax to return true whenever tutor_ajax_action is in the request.
  • REST. Core registers 13 routes in the tutor/v1 namespace: read-only GET routes for courses, topics, lessons, quizzes, ratings and author data, plus the e-commerce webhook. Read routes authenticate with API keys generated under Tools > Rest API and sent as a Basic Authorization header. Pro adds 48 route and method registrations for writes, enrolments and students. The courses, course-category and course-tag objects also appear in the core wp/v2 API because they are registered with show_in_rest.
  • Shortcodes and blocks. [tutor_dashboard], [tutor_course], [tutor_cart], [tutor_checkout] and others, plus the registration and cart-button blocks.

One REST detail matters on some hosts: the key check reads the header through apache_request_headers(), so a server that strips Authorization before PHP breaks key authentication.

How the data is split

Tutor uses WordPress tables for content and people, and custom tables where row-per-post would not scale.

DataWhere it lives
Course, topic, lesson, quiz, assignmentwp_posts: courses > topics > lesson / tutor_quiz / tutor_assignments, linked by post_parent
EnrolmentOne tutor_enrolled post per enrolment: post_author is the student, post_parent the course, status completed, pending or cancel
Lesson progressUser meta _tutor_completed_lesson_id_{lesson_id}
Course completionA course_completed comment whose content is the 16-character completion hash
Quiz questions and attemptstutor_quiz_questions, tutor_quiz_question_answers, tutor_quiz_attempts, tutor_quiz_attempt_answers
Native commercetutor_orders, tutor_order_items, tutor_ordermeta, tutor_coupons, tutor_carts and related tables
Revenue sharingtutor_earnings, tutor_withdraws
SettingsOne serialized option, tutor_option

Two consequences for integrations. First, quiz questions are not posts, so post-based duplication or migration tools miss them. Second, several commerce tables use real MySQL foreign keys, which matters for partial restores and table-by-table exports. The WordPress LMS custom post types article compares this layout with the other plugins.

Extension points: hooks, models and settings filters

Core 4.1.3 contains 844 unique hook names. The ones most integrations need:

EventHookArguments
Enrolment confirmedtutor_after_enrolled$course_id, $user_id, $enrolled_id
Enrolment row written (pending or completed)tutor_after_enroll$course_id, $enrolled_id
Lesson completedtutor_lesson_completed_after$lesson_id, $user_id
Course completedtutor_course_complete_after$course_id, $user_id
Quiz finishedtutor_quiz_finished$attempt_id, $quiz_id, $user_id
Block an enrolmentfilter tutor_allow_course_enrollmentReturn a WP_Error to block

Two behaviours are easy to miss. tutor_after_enrolled fires only for completed enrolments, while tutor_after_enroll also fires for pending ones created for purchasable courses. And Utils::get_option() runs apply_filters( $key, $value ) on every setting key, so each key in tutor_option is also an unprefixed filter name, such as monetize_by. That is convenient for overrides and a collision risk with other plugins that use the same generic names.

Templates and the 4.0 learning area

tutor_get_template() resolves a template name by looking in the child theme's tutor/ folder, then the parent theme's, then the plugin's templates/ folder, and finally Pro's when requested. Dots in the name become folders, so single.course.reviews maps to single/course/reviews.php. The tutor/ folder name is hard-coded: tutor()->template_path is defined but not read, so use the tutor_get_template_path filter to move a template.

The Template class takes over the course archive, single course, private video, public profile and dashboard through template_include at priority 99. Returning false from tutor_lms_should_template_override switches all of that off.

Version 4.0 added three learning modes, stored in setting learning_mode: Modern (the default), Kids and Legacy. In Modern and Kids mode, lessons, quizzes and assignments render through templates/learning-area/index.php, and the tutor_single_content_template filter applies only in Legacy mode. Theme overrides of the old single-lesson and single-quiz templates therefore stop applying after a switch to Modern mode.

Where Tutor LMS Pro plugs in

Pro is a separate plugin with its own namespaces (TUTOR_PRO and TutorPro\*). Its TUTOR_PRO\Init class checks that tutor/tutor.php is active and that the core version is at least TUTOR_CORE_REQ_VERSION (4.1.3 in this release) by reading the core plugin's Version header. Only then does it attach its loader to tutor_loaded. If the check fails, Pro shows an admin notice and loads nothing, so update core before Pro.

The loader always creates the admin and asset classes, applies a licence check in wp-admin, fires tutor_pro_before_load, instantiates the always-on Pro features, then includes every folder under addons/ and fires tutor_{addon}_addon_loaded for each. Each add-on's constructor checks its own on/off state, which core stores in option tutor_addons_config and changes through Addons::addon_enable_disable(). Enabling some add-ons creates their tables on tutor_addon_before_enable_{$basename}, so switch add-ons on through the admin screen, not by editing the option directly.

Architecture decisions to make before you customise

  • Load timing. Hook plugins_loaded or later and check for tutor(); do not assume tutor_loaded fires after your plugin is included.
  • Model calls over the bridge. Call Tutor\Models\* classes directly so typos fail loudly.
  • Learning mode first. Decide Modern or Legacy before writing template overrides; the override paths differ.
  • Reporting joins. Plan reports across wp_posts, wp_comments, wp_usermeta and the quiz tables, and filter enrolments on completed status, because raw SQL also sees pending rows.
  • Licence wording. The core plugin header says GPLv2 or later and its readme says GPLv3; if licensing matters for your distribution, check both files in the version you ship.

For how this compares with other plugins' internals, see the LifterLMS architecture and LearnPress architecture overviews, and the WordPress LMS development guide.

How we can help

We design and build integrations, data migrations and performance work on WordPress LMS stacks, including Tutor LMS. See our Tutor LMS and WordPress LMS pages. For hands-on Tutor LMS site setup and configuration, Tutor LMS Expert covers the step-by-step side.

Frequently asked questions

What is the main class in Tutor LMS?

TUTOR\Tutor in classes/Tutor.php. tutor.php stores its singleton in $GLOBALS['tutor'] through tutor_lms(). Its constructor registers the custom tables on $wpdb, creates the components and fires tutor_loaded.

What is the difference between tutor() and tutor_utils()?

tutor() returns the TUTOR\Config object with paths, version, nonce names and post type names. tutor_utils() returns a cached TUTOR\Utils instance with helper methods, and forwards unknown methods to the course, lesson, quiz, withdraw and enrolment models.

Which hook should a plugin use to extend Tutor LMS?

Tutor fires tutor_loaded while its own plugin file is included, so a plugin loaded after it misses that hook. Hooking plugins_loaded and checking that tutor() exists works regardless of load order. For events, use hooks such as tutor_after_enrolled and tutor_course_complete_after.

Does Tutor LMS store quiz questions as posts?

No. Quizzes are tutor_quiz posts, but questions and answers are rows in tutor_quiz_questions and tutor_quiz_question_answers, and attempts are in tutor_quiz_attempts. Tools that copy only posts miss them.

Free 30-minute consultation

Not sure where to start? Talk to an LMS engineer.

Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.

Sources & references

  1. Tutor LMS plugin on WordPress.org - WordPress.org
  2. Tutor LMS source repository - Themeum (GitHub)
  3. Plugin API: actions and filters - WordPress Developer Resources
Add as a preferred source on Google

Add All-in-One LMS as a preferred source to see more of our articles in Google’s Top Stories.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Keep reading

Before you go - get a free LMS audit

Tell us which LMS you use and what is not working. An engineer replies within one business day.

Choose which optional cookies we may use. You can change this at any time from "Cookie preferences" in the footer. Cookie policy · Privacy policy