Skip to main content

Creating a Module

Step-by-step guide to creating a new module for the ION Guard platform.

Prerequisites

  • Familiarity with Module System
  • Development environment with Laravel Sail configured
  • DevTools module enabled (for package generation)

Quick start

1. Create the directory structure

Create the module directory at app/Modules/{ModuleName}/:

app/Modules/MeuModulo/
├── module.json
├── MeuModuloModule.php
├── MeuModuloServiceProvider.php
├── routes/
│ ├── web.php
│ └── api.php
├── database/
│ └── migrations/
└── resources/
└── js/
├── entry.ts
└── pages/

2. Create the module.json

{
"identifier": "meu-modulo",
"name": "Meu Módulo",
"version": "1.0.0",
"description": "Descrição do que o módulo faz.",
"scope": "site",
"authors": [
{ "name": "Sua Empresa", "email": "dev@empresa.com" }
],
"requires": [],
"laravel": "^12.0"
}
FieldDescription
identifierUnique module slug (kebab-case). Used in routes, cache, middleware
scope"site" = enabled per site. "global" = always active
requiresDependencies on other modules (array of identifiers)

3. Create the module class

MeuModuloModule.php — implements ModuleContract (or extends BaseModule):

<?php

declare(strict_types=1);

namespace App\Modules\MeuModulo;

use App\Contracts\BaseModule;

final class MeuModuloModule extends BaseModule
{
public function identifier(): string
{
return 'meu-modulo';
}

public function name(): string
{
return 'Meu Módulo';
}

public function description(): string
{
return 'Descrição do módulo.';
}

public function version(): string
{
return '1.0.0';
}

public function scope(): string
{
return 'site';
}
}

BaseModule provides default implementations (empty arrays) for all optional methods. Override as needed.

4. Create the ServiceProvider

MeuModuloServiceProvider.php:

<?php

declare(strict_types=1);

namespace App\Modules\MeuModulo;

use App\Modules\ModuleRegistry;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

final class MeuModuloServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->make(ModuleRegistry::class)
->register(new MeuModuloModule());
}

public function boot(): void
{
$this->loadMigrationsFrom(__DIR__.'/database/migrations');

Route::middleware(['web', 'auth', 'module:meu-modulo'])
->group(__DIR__.'/routes/web.php');

Route::middleware(['api', 'auth:sanctum', 'module:meu-modulo'])
->prefix('api/v1')
->group(__DIR__.'/routes/api.php');
}
}

5. Create routes

routes/web.php:

<?php

use App\Modules\MeuModulo\Http\Controllers\Web\MeuController;
use Illuminate\Support\Facades\Route;

Route::get('/meu-modulo', [MeuController::class, 'index'])
->name('meu-modulo.index');

The module:meu-modulo middleware ensures that the routes are only accessible when the module is enabled for the current site.

6. Create the frontend entry point

resources/js/entry.ts:

// Auto-discovery of Inertia pages
const rawPages = import.meta.glob('./pages/**/*.tsx');
export const pages: Record<string, () => Promise<unknown>> = {};
for (const [filePath, resolver] of Object.entries(rawPages)) {
const name = filePath.replace(/^\.\/pages\//, '').replace(/\.tsx$/, '');
pages[name] = resolver;
}

// i18n translation registration
export function registerLocales(): void {
// Import and register translation bundles here
}

// Extensible slot registration
export function registerSlots(): void {
// Register components in core slots here
}
Page naming

Folders inside pages/ must follow kebab-case and match the module identifier. Example: pages/meu-modulo/index.tsx.

Extension points

After the basic structure, add functionality by overriding methods from ModuleContract:

Alert Handlers

public function alertHandlers(): array
{
return [
MeuAlertHandler::class,
];
}

public function alertTypes(): array
{
return ['meu-alerta'];
}

See Alert Handlers for detailed implementation.

Report Handlers

public function reportHandlers(): array
{
return [
MeuRelatorioHandler::class,
];
}

See Report Handlers for detailed implementation.

Add items to the side menu in the ServiceProvider's boot(), resolving the core's NavigationRegistry and registering a NavigationGroup with its NavigationItems. Tie the group/items to the module via the module: parameter — this way the menu only appears on the sites where the module is enabled (the NavigationRegistry checks isEnabledForSite, and isFeatureEnabledForSite when feature: is provided):

use App\Navigation\NavigationGroup;
use App\Navigation\NavigationItem;
use App\Navigation\NavigationRegistry;

public function boot(): void
{
// ... (routes, migrations)

$nav = $this->app->make(NavigationRegistry::class);

$group = $nav->group(new NavigationGroup(
id: 'meu-modulo',
title: 'Meu Módulo',
order: 50,
requiresSite: true,
module: 'meu-modulo',
translationKey: 'meu_modulo:nav.group',
));

$group->addItem(new NavigationItem(
title: 'Dashboard',
href: '/meu-modulo',
icon: 'layout-dashboard',
order: 10,
requiresSite: true,
module: 'meu-modulo',
translationKey: 'meu_modulo:nav.dashboard',
));
}

NavigationItem and NavigationGroup also accept minimumRole:, gate:, and feature: to control visibility by profile, permission, or sub-resource.

navigationGroups() does not exist

Navigation is registered imperatively in the NavigationRegistry (as above), not by a navigationGroups() method on the contract — that method does not exist on the platform.

Dynamic relationships

Extend core models without modifying them:

// In the ServiceProvider's boot()
Site::resolveRelationUsing('meuModuloConfig', function (Site $site) {
return $site->hasOne(MeuModuloConfig::class);
});

Scheduled tasks

public function schedule(Schedule $schedule): void
{
$schedule->call(function (): void {
// Check whether the module is enabled for each site
foreach (Site::query()->get() as $site) {
if (app(ModuleRegistry::class)->isEnabledForSite('meu-modulo', $site->id)) {
dispatch(new MeuJob($site->id));
}
}
})->everyFiveMinutes();
}
Required guard clause

Scheduled jobs must check whether the module is enabled for the site before executing. The central scheduler calls schedule() for all registered modules, regardless of their enablement state.

Workers (long-running processes)

public function workers(): array
{
return [
new WorkerDefinition(
name: 'meu-listener',
command: 'meu-modulo:listen',
),
];
}

Workers are converted into Docker containers by the RegenerateModuleRuntimeCompose action.

Tests

Structure the tests inside the module:

app/Modules/MeuModulo/
└── tests/
├── Feature/
│ └── MeuControllerTest.php
└── Unit/
└── MeuServiceTest.php

Tests are automatically discovered by Pest/PHPUnit. Run with:

sail artisan test app/Modules/MeuModulo/tests/

Packaging

To distribute the module as an installable package:

  1. Go to DevToolsModule Generator in the interface
  2. Select the module
  3. The system builds the frontend (if applicable), packages it into a ZIP, and encrypts it

The .enc package is installed via the Modules interface by a Platform Admin: the core (App\Actions\InstallModule + App\Services\ModuleReleaseEncryption) decrypts the package, validates the ZIP structure, and publishes the assets. The workers declared by the module become containers via RegenerateModuleRuntimeCompose.

Package generation

The package generation flow (build + packaging + encryption, e.g., via a DevTools module) is a development tool and may vary depending on the environment. What the core guarantees is the installation side of the .enc described above.

Pre-distribution checklist

  • module.json with correct metadata
  • ServiceProvider registers the module in ModuleRegistry
  • module:{identifier} middleware on all routes
  • Migrations create and remove tables correctly
  • Morph map registered (if using polymorphic relationships)
  • Policies registered for all models
  • Alert handlers fully implement AlertHandlerContract
  • Jobs check module enablement (guard clause)
  • Frontend entry.ts exports pages, registerLocales, registerSlots
  • i18n translations in pt_BR and en
  • Tests covering happy path and edge cases
  • README.md with description and instructions
  • Documentation in docs/ (user-guide.md, api.md, architecture.md)