Skip to main content
Wintertrace
Browse the documentation

Building modules

The module manifest (module.json)

Every module is described by a module.json. The fields it must declare, what each one controls, and how the manifest drives discovery and compatibility.

Every module needs a module.json in its root directory. This file is the module’s identity card: it tells the ModuleManager who the module is, which class boots it, and what it depends on. Without it, the directory is simply skipped during discovery.

Full schema

{
    "name": "My Module",
    "version": "1.0.0",
    "namespace": "Schneespur\\Module\\MyModule",
    "service_provider": "Schneespur\\Module\\MyModule\\MyModuleServiceProvider",
    "description": "Short description of what this module does.",
    "min_schneespur_version": "1.0.0",
    "requires_permissions": [],
    "default_enabled": false,
    "requires": {},
    "conflicts": []
}

Field reference

FieldTypeRequiredDescription
namestringyesHuman-readable module name. Shown in admin UI.
versionstringyesSemantic version (e.g. 1.2.3)
namespacestringyesPHP namespace for PSR-4 autoloading. Must match directory structure under src/.
service_providerstringyesFully-qualified class name of the module’s ServiceProvider
descriptionstringrecommendedShort description. Shown in module management UI.
min_schneespur_versionstringrecommendedMinimum compatible Schneespur/Wintertrace version
requires_permissionsstring[]optionalCore permissions the module needs to function
default_enabledbooleanoptionalWhether the module should be enabled after installation. Default: false
requiresobjectoptionalDependency map: {"other-module": "^1.0.0"}
conflictsstring[]optionalModule slugs that cannot be active simultaneously

default_enabled is intentionally false: a freshly installed module should not become active unnoticed. It runs only after deliberate activation in the module management UI.

Example from the reference module

{
    "name": "Example Module",
    "version": "1.0.0",
    "namespace": "Schneespur\\Module\\Example",
    "service_provider": "Schneespur\\Module\\Example\\ExampleServiceProvider",
    "description": "Reference module demonstrating all extension points.",
    "min_schneespur_version": "1.0.0",
    "requires_permissions": [],
    "default_enabled": false,
    "requires": {},
    "conflicts": []
}

Naming conventions

  • Module slug: The directory name under modules/. Use lowercase with hyphens: my-module, telegram-notifications
  • Namespace: Schneespur\Module\{PascalCaseName} — e.g. Schneespur\Module\TelegramNotifications
  • ServiceProvider class: {PascalCaseName}ServiceProvider — e.g. TelegramNotificationsServiceProvider

These three are linked: the slug is the directory name, the namespace determines the autoloader path, and the ServiceProvider class is the entry point named in the manifest. If any of these diverges from the directory structure, the autoloader cannot find the class.

Localised names and descriptions

For multi-language installations, name and description can be objects instead of strings:

{
    "name": {
        "de": "Telegram-Benachrichtigungen",
        "en": "Telegram Notifications"
    },
    "description": {
        "de": "Sendet Einsatz-Benachrichtigungen über Telegram.",
        "en": "Sends job notifications via Telegram."
    }
}

The Module model’s pickLocalized() method resolves to the current app locale or falls back to English.

Version constraints in requires

PatternExampleMatches
*"*"Any version
>=X.Y.Z">=2.0.0"2.0.0, 2.1.0, 3.0.0, …
^X.Y.Z"^1.2.0"1.2.0, 1.3.0, 1.99.0 (not 2.0.0)
~X.Y.Z"~1.2.0"1.2.0, 1.2.1, 1.2.99 (not 1.3.0)

These constraints are validated by the DependencyValidator before enabling — see Module lifecycle — Dependency validation.