For the complete documentation index, see llms.txt. This page is also available as Markdown.

How to start

Clone the module template and prepare it for development.

Every MikoPBX module follows the same skeleton: a module.json manifest, a PSR-4 autoloaded composer.json, and a handful of well-known directories that the Core discovers by convention. You never start from a blank directory. There are two supported ways to bootstrap a new module, and both produce the same layout.

Throughout this documentation we use a single running example — a fictional module called ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers backed by table m_BlackListNumbers, front-end asset module-black-list-index.js). Wherever a pattern is shown, you will also find a pointer to a real example module you can read in the repository.

Two ways to bootstrap a module

Path 1 — Clone the ModuleTemplate

ModuleTemplate is a complete, installable reference module that ships every common extension point already wired up. Cloning it gives you a guaranteed-correct starting layout.

git clone https://github.com/mikopbx/ModuleTemplate.git ModuleBlackList
cd ModuleBlackList
rm -rf .git

After cloning you rename the module identity (namespace, moduleUniqueID, classes, table names) to match your feature. The identity rules are described under moduleUniqueID is the spine below.

Path 2 — Generate with the /mikopbx-module skill

If you work with an AI coding agent (Claude Code, Codex, Cursor, Gemini CLI, …), the /mikopbx-module skill from mikopbx/agent-skills generates a fully wired module from a natural-language description. It produces the same layout as the template, picks the integration "recipes" you actually need (UI, REST API, dialplan hooks, workers, AGI, firewall, …), and follows the naming conventions automatically.

/mikopbx-module Create a module that blocks incoming calls from a blacklist of numbers,
with a settings page and a REST API for managing the list.

See Using the skill for the full workflow.

Both paths converge on the structure described on this page. Read Module anatomy afterwards for the complete, file-by-file map.

The ModuleTemplate root layout

The template root contains everything the Core expects, plus empty extension points (directories with a single .gitkeep) that you fill in only if your module needs them.

The three .gitkeep-only directories — agi-bin/, bin/, and db/ — are extension points. They keep the directory in version control so the path exists the moment you need it, without forcing every module to carry AGI scripts, workers, or seed data it does not use.

You can confirm this layout against the shipped template at Extensions/ModuleTemplate/, and against a focused, real-world UI module at Extensions/EXAMPLES/WebInterface/ModuleExampleForm/.

moduleUniqueID is the spine

A single value — moduleUniqueID in module.json — drives almost every other name in the module. Choose it once, in PascalCase, prefixed with Module (our example uses ModuleBlackList), and everything else follows by convention.

The Core derives the module's identity from this value. In particular, every module class lives under the namespace Modules\{moduleUniqueID}\…, and the Core resolves the running module's ID from exactly that namespace segment. See PbxExtensionBase::__construct() in src/Modules/PbxExtensionBase.php, which splits the class namespace and reads the second segment:

What moduleUniqueID controls:

Concern

Derived value

Example (ModuleBlackList)

PHP namespace

Modules\{ID}\…

Modules\ModuleBlackList\Lib

On-disk module directory

{modulesDir}/{ID}

…/Modules/ModuleBlackList

Database table prefix

m_{Entity} (per model)

m_BlackListNumbers

URL slug (routes, assets, REST)

module-{kebab-case}

module-black-list

Admin-cabinet route

/{slug}/{controller}/{action}

/module-black-list/module-black-list/index

The database table name is set per-model with setSource(). In the template's model Models/ModuleTemplate.php it is $this->setSource('m_ModuleTemplate'); for our example it would be m_BlackListNumbers.

The admin route is registered by the Core as /{uncamelized-uniqueID}/:controller/:action/:params — see PbxExtensionUtils::registerEnabledModulesInRouter() in src/Modules/PbxExtensionUtils.php, which builds the slug with Text::uncamelize($moduleUniqueId, '-'). Do not confuse this with $this->view->currentPage, which BaseController assembles as "$module->uniqid/$controllerName/$actionName" (PascalCase) purely to highlight the active menu entry — it is not a URL.

For the full table of class, file, table, route, and translation-key conventions, see Module anatomy.

Modernize the manifest for a new module

The shipped Extensions/ModuleTemplate/module.json and composer.json still target an older platform — min_pbx_version: 2023.2.150 and PHP ^7.4 || ^8.0. Do not keep those values in a new module. Update them to the current baseline.

module.json

Set min_pbx_version to 2025.1.1 for new modules. This matches the current example modules — see Extensions/EXAMPLES/WebInterface/ModuleExampleForm/module.json, which declares "min_pbx_version": "2025.1.1".

%ModuleVersion% is a placeholder substituted by the release tooling at build time; leave it as-is in source. Every field is documented in detail on the module.json reference page.

composer.json

Match the PHP and Core constraints to the current platform. MikoPBX Core 2025.1.1+ declares "php": "^8.4" in its own composer.json, so a module that depends on Core must require the same.

Install the mikopbx/core dependency

Pull in mikopbx/core and its dependencies so your IDE and static analysis can resolve MikoPBX class names, hooks, and ORM models.

Read Prepare IDE and system tools first — it covers installing the PHP toolchain, Composer, and the IDE configuration the next steps assume.

This populates vendor/ with the Core sources. You exclude vendor/ from both the module ZIP and version control — it is a development-time aid only, never shipped.

Put the module under version control

If you have a remote repository, push to it:

Try it on MikoPBX

Once the layout and module.json are in place you can package the module as a ZIP and install it through the admin cabinet (Extension modules → Install module). The PBX unpacks it, runs Setup/PbxExtensionSetup.php, creates the module's database tables from your Models/, and registers its menu and assets.

You do not need any custom code to get a first install working — the cloned template is installable as-is. Iterate from there: rename the identity, then add only the extension points your feature needs.

Next steps

Last updated

Was this helpful?