Skip to content

Module Development ​

A module is a folder with a module.json and a Laravel service provider. It can add pages to the creator panel, the admin panel and the public site, store its own data, react to quiz attempts and run scheduled tasks, all without editing Quizora.

This page is the contract between Quizora and modules. Only what's listed here is stable. Anything else in Quizora is internal and may change in any release. Changing an item on this list is a breaking change: it bumps Quizora's major version, and modules declare which versions they support in module.json ("requires": {"product": "^1.5"}).


Quick start ​

On a development copy of Quizora:

  1. Turn on developer mode in .env (loads unsigned modules from modules-dev/):

    bash
    LICENTRA_MODULES_DEV=true
  2. Get the example module: Admin → Configuration → Modules → More → Download example module, then Upload module with the zip (or unzip it into modules-dev/). Or start from scratch:

    bash
    php artisan module:make certificates-plus   # creates modules-dev/quizora-certificates-plus/
  3. Create its tables and publish its CSS/images:

    bash
    php artisan module:migrate quizora-hello
  4. Give it to a plan (Admin → Plans → Modules included) and open its pages.

Never on a live site

Developer mode loads code that isn't checked.


Anatomy of a module ​

The example module, quizora-hello:

quizora-hello/
├── module.json                         identity, versions, what it touches
├── src/
│   ├── ModuleServiceProvider.php       entry point: routes, views, hooks, tasks, panel pages
│   ├── Models/Greeting.php             its own model
│   └── Filament/
│       ├── CreatorPlugin.php           pages for the creator panel (/creator)
│       ├── Creator/Greetings.php       a page with a form
│       ├── AdminPlugin.php             pages for the platform admin (/admin)
│       └── Admin/Overview.php
├── routes/web.php                      public pages, under /quizora-hello/...
├── resources/views/                    view('quizora-hello::public')
├── lang/en/messages.php                __('quizora-hello::messages.title')
├── database/migrations/                its own tables
└── public/hello.css                    published to /modules/quizora-hello/hello.css

The provider:

php
class ModuleServiceProvider extends \Pacific\Licentra\Modules\Laravel\ModuleServiceProvider
{
    public function bootModule(): void
    {
        $this->loadModuleResources();                             // views + translations
        $this->loadModuleRoutes('routes/web.php', ['web']);       // under /quizora-hello, gated per plan

        $this->observe(Attempt::class, 'created', function (Attempt $attempt) {
            Greeting::create(['creator_id' => $attempt->quiz->creator_id, 'message' => 'New attempt!']);
        });

        $this->schedule(fn ($schedule) => $schedule->call(fn () => Greeting::prune())->daily());
    }

    public function filamentPlugins(string $panel): array
    {
        return match ($panel) {
            'creator' => [CreatorPlugin::make()],
            'admin'   => [AdminPlugin::make()],
            default   => [],
        };
    }
}

module.json ​

json
{
    "manifest": 1,
    "slug": "quizora-hello",
    "name": "Hello (example module)",
    "description": "Shown on the Modules page.",
    "version": "1.0.0",
    "product": "quizora",
    "requires": { "product": "^1.5", "licentra": "^1.5" },
    "namespace": "Modules\\QuizoraHello\\",
    "provider": "Modules\\QuizoraHello\\ModuleServiceProvider",
    "envato_item_id": null,
    "plan_feature": true,
    "capabilities": {
        "tables": ["quizora_hello_greetings"],
        "outbound": [],
        "routes": ["GET /quizora-hello/{creator}"],
        "schedule": true
    }
}
KeyNotes
slugquizora-{name}, lowercase. The same slug is used everywhere: folder, URLs, view namespace, license.
versionx.y.z. An older version can never replace a newer one.
requires.productQuizora versions you support (^1.5, >=1.5 <2.0). On other versions the module is switched off, not broken.
namespace, providerPSR-4 root for src/; the provider must be inside it.
plan_featuretrue: sold per plan (a checkbox on plans and users). false: every creator gets it.
capabilitiesWhat the module adds, shown to the admin before install. Be accurate; it isn't enforced.

Pages: creator, admin and public ​

Creator panel and admin panel (Filament 3). Add pages and resources in a Filament plugin returned from filamentPlugins(). Every module page and resource must:

php
use Pacific\Licentra\Modules\Laravel\GatedByModule;

class Greetings extends \Filament\Pages\Page
{
    use GatedByModule;                                     // hidden unless the module runs and the plan includes it

    protected static string $module = 'quizora-hello';
    protected static ?string $slug = 'quizora-hello/greetings';   // must start with your slug
}

A page whose slug doesn't start with the module slug gets the module switched off, so a module can never take over a Quizora page.

Public pages. Put routes in routes/web.php and load them with loadModuleRoutes(). Everything is placed under /{slug}/ and named {slug}.*:

php
// routes/web.php → GET /quizora-hello/{creator}, route name "quizora-hello.public"
Route::get('/{creator}', fn (User $creator) => view('quizora-hello::public', compact('creator')))
    ->name('public');

With a {creator} parameter the plan check uses that creator; otherwise it uses the logged-in creator. Add 'auth' to the middleware list for logged-in pages.


Hooking into Quizora safely ​

React to Quizora only through these helpers. If your callback throws, Quizora's own action (saving the attempt, rendering the page) still completes; the error is logged and charged to your module.

HelperExample
observe($model, $events, $fn)$this->observe(Attempt::class, 'created', fn ($attempt) => …)
listen($events, $fn)$this->listen(SomeEvent::class, fn ($event) => …)
composer($views, $fn)$this->composer('welcome', fn ($view) => $view->with(…))
schedule($fn)$this->schedule(fn ($s) => $s->call(fn () => …)->daily())

A module is switched off when it throws while starting, causes 3 PHP crashes in 10 minutes, causes 3 errors on Quizora's pages (code hooked in some other way), or causes 10 errors in 10 minutes on its own pages, hooks or tasks. 404s, validation errors and redirects to login don't count.


Data and creators ​

  • Everything a module stores belongs to one creator: a creator_id column (foreignUuid('creator_id')->constrained('users')). User ids are UUIDs.
  • Always scope queries by creator (where('creator_id', auth()->id()) in the creator panel). Never trust an id from the browser on its own.
  • Prefix your tables with the slug in snake case (quizora_hello_…). Never add columns to Quizora's tables.
  • Migrations run when the module is installed or updated, never during Quizora's own updates. Uninstalling keeps the tables unless the admin chooses to delete the data.

The module API (v1) ​

AreaStable API
ProviderExtend Pacific\Licentra\Modules\Laravel\ModuleServiceProvider: register(), bootModule(), filamentPlugins($panel), loadModuleRoutes(), loadModuleResources(), observe(), listen(), composer(), schedule(), modulePath()
FilamentPanel ids admin and creator; GatedByModule trait; page/resource slugs {slug}/…
Plan gatingplan_feature in module.json; app(\Pacific\Licentra\Modules\Laravel\TenantGate::class)->allows($slug, request())
ModelsApp\Models\User (roles super_admin, creator, customer), Quiz, Question, Attempt, QuizEnrollment, Order, Plan, Subscription: read, relate, and observe created / updated / deleted
PackagesOnly what Quizora already requires (Laravel 12, Filament 3, Livewire 3). Modules can't ship a vendor/ folder.

Rules ​

  • Store everything per creator (creator_id).
  • Never edit files outside your module folder.
  • Keep capabilities accurate.
  • Static files go in public/; only CSS, JS, images, fonts and JSON are published (never PHP).

Testing ​

Run your module's tests through Quizora's test runner, loading the module from a scratch folder so your real modules-dev/ is never touched. Quizora's own tests/Feature/ModulesSmokeTest.php shows the setup with the example module.


Releasing ​

  1. In the Licentra admin, add a product: slug quizora-{name}, Add-on for: Quizora, pricing Envato (with its CodeCanyon item ID) or Free.

  2. Bump version in module.json, then from the Quizora folder:

    bash
    vendor/bin/licentra-release module modules-dev/quizora-{name} --upload

    This signs the module with your release key, uploads it, and also writes quizora-{name}-x.y.z.licentra-module.zip, a signed package admins can install with Upload module.

  3. Publish the release in the Licentra admin. Every licensed Quizora shows it under Available add-ons (or as an update).

Quizora v1.5.1 — AI-Powered Quiz Platform