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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Human-readable module name. Shown in admin UI. |
version | string | yes | Semantic version (e.g. 1.2.3) |
namespace | string | yes | PHP namespace for PSR-4 autoloading. Must match directory structure under src/. |
service_provider | string | yes | Fully-qualified class name of the module’s ServiceProvider |
description | string | recommended | Short description. Shown in module management UI. |
min_schneespur_version | string | recommended | Minimum compatible Schneespur/Wintertrace version |
requires_permissions | string[] | optional | Core permissions the module needs to function |
default_enabled | boolean | optional | Not read by the core. See the note below |
requires | object | optional | Dependency map: {"other-module": "^1.0.0"} |
conflicts | string[] | optional | Module slugs that cannot be active simultaneously |
default_enabled has no effect. No code path reads it. Both routes that create a modules row — installing through the admin interface, and the install branch of ModulesSync — write enabled = true regardless. Since 1.2.0 that row is the only thing that decides whether a module boots at all; see Lifecycle.
So setting it to false and expecting the module to install dormant will not do what you want. The field is kept because manifests in the wild carry it. Treat it as documentation of intent, not as behaviour.
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
| Pattern | Example | Matches |
|---|---|---|
* | "*" | 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.