Create a Module
Create a Module
Section titled “Create a Module”This guide is for contributors who want to create a standalone Composer module for KoAkademy and maintainers who will review and publish it. A complete module release has three separate deliverables:
- A tested module repository with a matching semantic version tag.
- A signed entry in the KoAkademy module registry.
- A KoAkademy application image that installs the locked package.
The Marketplace only displays the signed catalog and controls the status of code already installed in the image. It does not install Composer packages or update a live container.
1. Decide the module boundary
Section titled “1. Decide the module boundary”Create a module when the feature is an optional domain with its own models, migrations, screens, permissions, routes, jobs, or scheduled work. Keep the module independent from the core application where practical:
- Put module code, tests, migrations, configuration, and views in the module repository.
- Use public contracts for host integrations instead of reaching into arbitrary application classes.
- Treat student, medical, financial, and identity data as sensitive. Define authorization, retention, audit, and export behavior before implementation.
- Do not change core tables or assume a particular host model without declaring the integration contract and migration impact.
- Use database migrations for every schema change. Never rewrite a migration that has been released.
Review the system module guide and the Forms module for examples of a package with host-model integration.
2. Choose stable identities
Section titled “2. Choose stable identities”Choose these values once and keep them stable after the first release:
| Identity | Rule | Example |
|---|---|---|
| GitHub repository | koakademy-module-<alias> | koakademy-module-surveys |
Module name | Starts with a letter; letters, numbers, _, and - | Surveys |
Module alias | Lowercase letters, numbers, and - | surveys |
| Composer package | Lowercase vendor/package | koakademy/surveys |
| PHP namespace | Modules\\<Name>\\ | Modules\\Surveys\\ |
| Provider | A loadable service-provider class | Modules\\Surveys\\Providers\\SurveysServiceProvider |
The module name, alias, Composer package, provider, and repository URL identify the same product across Composer, Laravel, the catalog, and Marketplace. Do not rename them as part of a routine feature release.
3. Create the standalone repository
Section titled “3. Create the standalone repository”Create a public repository and a local checkout. The repository can start from an existing standalone module’s file layout, but remove its domain code and tests rather than copying behavior accidentally:
gh repo create yukazakiri/koakademy-module-surveys --public --clonecd koakademy-module-surveyscomposer init \ --name=koakademy/surveys \ --type=library \ --license=AGPL-3.0-or-later \ --no-interactionmkdir -p app/Providers app/Models app/Policies database/migrations resources/views routes tests/Unit tests/FeatureAdd the normal repository files:
koakademy-module-surveys/├── app/│ ├── Models/│ ├── Policies/│ └── Providers/SurveysServiceProvider.php├── config/├── database/migrations/├── resources/views/├── routes/├── tests/├── composer.json├── module.json├── phpunit.xml├── README.md└── LICENSE.mdKeep vendor/, build output, local environment files, and test databases out
of Git. The release asset is generated from the public Git tag by GitHub.
4. Add the module manifest
Section titled “4. Add the module manifest”Create module.json at the repository root. This is the metadata read by
KoAkademy and by the registry updater:
{ "name": "Surveys", "alias": "surveys", "composer_package": "koakademy/surveys", "version": "0.1.0", "description": "Configurable surveys for school workflows.", "author": "KoAkademy contributors", "license": "AGPL-3.0-or-later", "requires": { "core": ">=1.22.0", "php": ">=8.5", "modules": {} }, "compatibility": { "laravel": "^13.0", "filament": "^5.0" }, "providers": [ "Modules\\Surveys\\Providers\\SurveysServiceProvider" ], "repository": "https://github.com/yukazakiri/koakademy-module-surveys", "homepage": "https://github.com/yukazakiri/koakademy-module-surveys"}The required registry fields are name, alias, composer_package,
version, description, author, license, repository, homepage, and
providers. requires.modules is an object whose keys are module names and
whose values are version constraints when another module is required.
Keep module.json and composer.json versions aligned. The first release can
use 0.1.0; subsequent releases should follow semantic versioning. The tag
must be v0.1.0 for this example.
5. Wire Composer and Laravel discovery
Section titled “5. Wire Composer and Laravel discovery”The package must autoload its namespace and advertise its provider through Composer. A minimal runtime section looks like this:
{ "name": "koakademy/surveys", "type": "library", "license": "AGPL-3.0-or-later", "require": { "php": ">=8.5 <8.6", "filament/filament": "^5.0", "laravel/framework": "^13.0", "nwidart/laravel-modules": "^13.0" }, "autoload": { "psr-4": { "Modules\\Surveys\\": "app/", "Modules\\Surveys\\Database\\Factories\\": "database/factories/", "Modules\\Surveys\\Database\\Seeders\\": "database/seeders/" } }, "autoload-dev": { "psr-4": { "Modules\\Surveys\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "Modules\\Surveys\\Providers\\SurveysServiceProvider" ] } }, "scripts": { "test": "pest --compact" }}Only add runtime dependencies the module actually uses. Add inertiajs,
spatie/laravel-permission, storage adapters, or other packages explicitly
when the module needs them. Do not copy the entire KoAkademy application’s
dependency list into every module.
The service provider should load only the module’s own resources and bind documented integration contracts. For example:
<?php
declare(strict_types=1);
namespace Modules\Surveys\Providers;
use Illuminate\Support\ServiceProvider;
final class SurveysServiceProvider extends ServiceProvider{ public function register(): void { $this->app->register(RouteServiceProvider::class); }
public function boot(): void { $modulePath = dirname(__DIR__, 2);
$this->loadMigrationsFrom($modulePath.'/database/migrations'); $this->mergeConfigFrom($modulePath.'/config/surveys.php', 'surveys'); $this->loadViewsFrom($modulePath.'/resources/views', 'surveys'); }}Register policies, gates, commands, routes, translations, and Filament resources from the provider or a provider it registers. Keep authorization in the backend; hiding a frontend link is not access control.
6. Build the feature and its contract tests
Section titled “6. Build the feature and its contract tests”Implement the domain using the application’s Laravel conventions:
- migrations and Eloquent models own durable data;
- policies and permission checks protect every read and write;
- Filament resources belong under the module namespace;
- routes use the module prefix and named routes;
- queued work is safe to retry and does not leak sensitive payloads;
- uploaded files use private storage unless public access is explicitly part of the feature;
- host-specific behavior is behind an adapter or contract with a safe fallback for other Laravel hosts.
For record-linked modules, define the binding contract before the UI. Accept only allowlisted model keys and field paths; never accept a browser-submitted record ID as the authority for an update. If the host has sparse or evolving data, add a documented JSON fallback rather than changing the module’s public field keys on every host schema variation. If suggestions are exposed, return normalized values only and enforce a minimum frequency so one record cannot be identified.
At minimum, test:
- manifest and Composer package identity;
- service-provider discovery and boot;
- migrations on a clean database and an upgrade database;
- authorization for allowed and denied roles;
- validation and duplicate/submission behavior;
- any host-model mapping, tenant boundary, file access, export, or queue path.
For invitation-backed record updates, also test token hashing, expiry, revocation on resend, one-time completion, queued delivery, record binding, row locking, blank-only application, concurrent-value protection, and audit metadata for applied and skipped fields.
Run the package checks before tagging:
composer validate --no-check-publish --strictcomposer install --no-interaction --prefer-distcomposer testfind . -path './vendor' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -lThe module repository should also have a secret-free GitHub Actions workflow that runs Composer validation, dependency installation, syntax checks, and tests on pull requests and pushes. Do not make the registry signing key or production deployment credentials available to module CI.
7. Test it in KoAkademy
Section titled “7. Test it in KoAkademy”Before publishing, test the package from a KoAkademy checkout rather than only from the module’s package test harness:
composer config repositories.local-module path ../koakademy-module-surveyscomposer require koakademy/surveys:dev-mainphp artisan migrate --forcephp artisan package:discover --ansiphp artisan route:listnpm run buildphp artisan test --compactFor a real release candidate, test from the Git tag or ZIP distribution so
missing files and incorrect package metadata cannot be hidden by a path
repository. Confirm that vendor/koakademy/surveys/module.json exists and
that the provider class is discoverable.
Remove the local path repository before committing the application’s final Composer configuration. Production should resolve a tagged package from the public registry and use the committed lockfile.
8. Release the module
Section titled “8. Release the module”Update the module version and release notes, run the package checks again, then create the matching tag:
git add module.json composer.json composer.lock README.md CHANGELOG.mdgit commit -m "feat(surveys): release 0.1.0"git push origin maingit tag -a v0.1.0 -m "Release v0.1.0"git push origin v0.1.0gh release create v0.1.0 --repo yukazakiri/koakademy-module-surveys --generate-notesThe registry uses the GitHub tag archive at:
https://github.com/yukazakiri/koakademy-module-surveys/archive/refs/tags/v0.1.0.zipDo not change the tag contents after publication. If the release is defective, publish a new patch version rather than moving the tag.
9. Register it in the public catalog
Section titled “9. Register it in the public catalog”Clone the registry repository and download the exact tag archive. The updater
reads the module repository’s module.json and composer.json, computes the
SHA-256 catalog checksum and Composer SHA-1 distribution checksum, and updates
both indexes:
gh repo clone yukazakiri/koakademy-modulescd koakademy-modulescurl -L https://github.com/yukazakiri/koakademy-module-surveys/archive/refs/tags/v0.1.0.zip \ -o /tmp/surveys-v0.1.0.zipphp scripts/update-module.php \ --module=/path/to/koakademy-module-surveys \ --archive=/tmp/surveys-v0.1.0.zipphp scripts/validate-registry.phpgit diff -- registry.json packages.jsonOn Windows PowerShell:
gh repo clone yukazakiri/koakademy-modulesSet-Location koakademy-modules$moduleVersion = '0.1.0'Invoke-WebRequest ` -Uri "https://github.com/yukazakiri/koakademy-module-surveys/archive/refs/tags/v$moduleVersion.zip" ` -OutFile "$env:TEMP/surveys-v$moduleVersion.zip"php scripts/update-module.php ` --module='C:\path\to\koakademy-module-surveys' ` --archive="$env:TEMP/surveys-v$moduleVersion.zip"php scripts/validate-registry.phpgit diff -- registry.json packages.jsonReview the generated entry carefully:
- package name and module alias are correct;
- the current version is present in both files;
- the asset URL points to the immutable
v0.1.0tag; - the checksum was calculated from that exact URL’s archive;
- core, PHP, module, Laravel, and Filament requirements are correct;
- the provider class exists in the release;
- no private key or sensitive data is in the diff.
The updater removes registry.json.signature intentionally. Commit the two
generated index changes to a branch and open a pull request. Contributors do
not sign the catalog.
10. Maintainer signing and publication
Section titled “10. Maintainer signing and publication”After the registry pull request and module release are reviewed, a maintainer uses the existing private Ed25519 key from protected storage:
php scripts/validate-registry.phpphp scripts/sign-registry.php registry.json /secure/registry-private.key registry.jsonphp scripts/validate-registry.phpphp scripts/verify-registry.phpgit add registry.json packages.jsongit commit -m "feat(registry): publish surveys 0.1.0"git push origin masterRegistry CI validates structure on pull requests and verifies the signature on
master. GitHub Pages repeats both checks before publishing the catalog and
Composer index. Do not generate a new key for a normal module release; that
would invalidate existing applications’ configured public key.
11. Install it and make it appear in Marketplace
Section titled “11. Install it and make it appear in Marketplace”After the signed catalog is published, update the KoAkademy application repository:
composer config repositories.koakademy composer https://yukazakiri.github.io/koakademy-modulescomposer require koakademy/surveys:^0.1git add composer.json composer.lockgit commit -m "feat(modules): add surveys"git pushThe application image build must run Composer installation and the normal frontend build. The production deployment then runs the module’s migrations, clears/rebuilds caches, and rolls every Swarm replica. For example:
php artisan migrate --forcephp artisan optimize:clearphp artisan optimizeMarketplace visibility requires all of these conditions:
- The signed catalog entry is reachable at the configured HTTPS registry URL.
- The application’s configured public key matches the registry trust root.
- The module package is installed in the image and its
module.jsonis under the vendor scan path. MODULE_SCAN_VENDOR=trueremains enabled.- Composer’s Laravel provider discovery has run successfully.
- The module satisfies the running core/PHP/Laravel/Filament constraints.
- A super administrator opens Administrators → Marketplace.
The Marketplace combines catalog entries with installed manifests. It may show a catalog release before the package is installed, but it cannot enable a module that is absent from the image. A newly installed package may be disabled until the administrator enables it; the persistent status file keeps existing module choices during upgrades.
12. Update the module later
Section titled “12. Update the module later”For version 0.1.1, repeat the release and registry steps with tag v0.1.1.
Then update the application lockfile and image:
composer update koakademy/surveys --with-dependenciesphp artisan test --compactgit add composer.json composer.lockgit commit -m "chore(modules): update surveys"git pushThe registry update alone does not change vendor/ in an existing container.
Dokploy or the Swarm operator must deploy the newly built image. If the module
still exists as a local source-tree copy under Modules/, its standalone
repository releases do not update that copy; migrate to the Composer package
as a separate tested change.
Troubleshooting checklist
Section titled “Troubleshooting checklist”It is in the registry but not installed
Section titled “It is in the registry but not installed”Add the Composer requirement to the KoAkademy application, commit the lockfile, rebuild the image, and redeploy. Refreshing Marketplace cannot install it.
It is installed but not visible
Section titled “It is installed but not visible”Confirm vendor/<vendor>/<package>/module.json exists, MODULE_SCAN_VENDOR=true,
the provider is present in Composer’s extra.laravel.providers, and the image
ran php artisan package:discover --ansi. Clear application/Octane caches and
restart the app service after changing package contents.
It is visible but cannot be enabled
Section titled “It is visible but cannot be enabled”Read the compatibility error. Check the core/PHP/Laravel/Filament constraints, required modules, provider boot errors, migrations, and persistent module status. Enable required module dependencies first. Edge core versions include build metadata; compatibility checks normalize that prerelease/build suffix before comparing the core version constraint.
Registry signature verification fails
Section titled “Registry signature verification fails”Restore the existing private key or revert the unsigned catalog change. Do not generate a replacement key as a quick fix; existing installations trust the current public key and will reject a catalog signed by a new key.
Maintainer acceptance checklist
Section titled “Maintainer acceptance checklist”- The module has a stable name, alias, Composer package, provider, and public repository.
-
module.jsonandcomposer.jsonare complete and version-aligned. - Runtime dependencies and compatibility constraints are minimal and correct.
- Migrations, authorization, privacy, audit, and rollback behavior are reviewed.
- Package contract, integration, upgrade, and failure-path tests pass.
- Module CI is secret-free and green.
- The public
vX.Y.Ztag is immutable and matches the manifest. - Registry metadata passes validation and is reviewed before signing.
- The existing signing key is used; no private key is committed.
- A KoAkademy image test confirms provider discovery, migrations, build, and Marketplace enablement.