Skip to main content
Core finds most module code by building a class name as a string, so a typo means the feature silently doesn’t exist. Copy names from here rather than from memory, and run php artisan module:doctor to check.
Throughout this page, {M} is the name field from module.json, for example PterodactylService.

Service module classes

Only ModuleServiceActions and the three Actions\* classes are needed for a working module. Everything else gates an optional feature.
ServiceControlLoader always passes two arguments to getServiceControls(), so declare both even if you ignore them.
There used to be a Modules\{M}\Services\{M}Service class for termination. It never resolved, because the lookup produced names like PterodactylServiceService. Termination goes through Actions\DeleteServerService via ServiceManagementService::executeModuleAction(). Ignore that pattern if you see it in older code.

Gateway module classes

Gateway key: Str::lower(Str::before($moduleName, 'Gateway')), so AcmeGateway gives acme. getGatewayName() must return it.
Plus protected function getGatewayName(): string.
$paymentData['type'] is one of market, renewal, invoice, upgrade, downgrade, configurable_options_change, addon, balance, standalone.

OAuth provider modules

Discovered by App\Modules\OAuthProviderRegistry, which scans enabled modules whose name contains Provider.
registerViaOAuth() resolves by link column, then by email only if that account already carries the same provider ID, then creates. An email match on an unlinked account throws "...already exists but is not linked to {$label}", which each controller surfaces verbatim.Don’t relax that: matching on email alone is an account-takeover path.

Core base classes and interfaces

Core events

All post-events. None can veto or change the action.

Core jobs that call modules

Middleware

Route files

module.json fields

permissions shape

Node keys are stored in AdminRole.permissions JSON, so never rename one after release. Routes omit the /admin prefix. A node with no category is grantable to superadmins only.
Existing categories you can add to: gateway_settings (“Gateway & Service Settings”) and addon_module_settings (“Addon Module Settings”).

Settings page definition

Returned from settingsDefinition(). Rendered by the shared admin/ModuleSettings page.
string
required
Heading shown on the page.
string
required
One line under the heading.
string
required
Where the form POSTs. Use url(), not route().
array
required
array
Splits the form into tabs. Each entry takes key, label and optional description.
string
Adds a Test connection button to the header.
string
Overrides the button text.
string[]
Sidebar hints. Ones containing a URL get a copy button.
array
Sidebar panels. Take title, description, status, status_tone and rows.
module_name, display_name, logo, category, kind_label, back_url and back_label are filled in from module.json by ModuleSettingsController. You don’t set them.

Commands

make:payment-gateway still exists and works, but make:module --type=gateway supersedes it.

Things that still need a core change

Everything else, including admin routes, permissions, OAuth registration and provider branding, is declarable from the module itself.