Skip to main content

Start here

1

Scaffold it

--type takes service, gateway, provider or addon. Leave it off and the command asks.
2

Switch it on

3

Check it

You now have a working module: an admin settings page, routes, permissions, a credentials table, and stubs for everything core will call. Search it for TODO to find what’s left to fill in.
The generator runs composer dump-autoload for you and leaves the module disabled, which is the safe default. Read Enabling a module before you use --enable, because php-fpm caches the autoloader and there is a timing trap.

Which type do I want?

Service modules

Something people buy that you have to create: a game server, VPS, hosting account, domain or licence key.

Payment gateways

Taking payments through a new provider. Core already knows how to turn a payment into an invoice or a service.

OAuth providers

Sign in with Discord, Google, GitHub and friends. The smallest module type.

Addon modules

Anything else: affiliates, blogs, forms, compliance tooling, status pages.

The other pages

Fundamentals

The parts every module shares: manifest, providers, routes, permissions, settings UI, credentials, events. Read it once and you can read any module in the tree.

Conventions reference

Exact class names, method signatures, what calls what. Keep it open while you work.

Five things that will bite you

Core finds your code by building a string like Modules\AcmePanel\Services\RenewService. Spell it wrong and nothing happens. No error, no warning, the feature just doesn’t exist.php artisan module:doctor is how you catch that.
Some modules autoload through a classmap rather than PSR-4, and classmaps are static. Your new file stays invisible until you dump.Anything from make:module lives under app/, so PSR-4 covers it and you won’t hit this.
If the customer has paid and the server didn’t get created, throw an exception. Throwing gets the queued job retried. Returning ['success' => false] and logging a warning means nobody finds out until they complain.This is the most expensive class of bug in the product.
The existing credential models encrypt with Crypt::encryptString(). Laravel’s encrypted cast uses Crypt::encrypt(), which serialises. They are not compatible, so adding the cast makes every stored credential unreadable.New columns are fine. See Credentials.
Meaning: check the rest of the app does. Nothing in core should depend on your tables, listeners or routes.That’s the step people skip and the one that breaks production.

Commands

make:module also accepts --webhook for gateways and --enable to switch the module on and migrate it in one go.