Browse the documentation
Building modules
Architecture overview
How Wintertrace loads modules at boot and where a module plugs in: the registry-based extension model that lets modules add capabilities without changing core files.
Before you write a module, it helps to have a clear picture of what it connects to. Wintertrace is a Laravel application, and modules are not foreign bodies in it — they are regular Laravel ServiceProviders that register their capabilities at clearly defined points. This page shows those points.
Tech stack
| Layer | Technology |
|---|---|
| Framework | Laravel 11+ (PHP 8.2+) |
| Database | MySQL / MariaDB (production). SQLite :memory: only for the test suite |
| Frontend | Blade templates, Tailwind CSS, Alpine.js |
DomPDF (swappable via PdfRendererRegistry) | |
| GPS | OwnTracks integration (MQTT/HTTP) |
| Weather | OpenMeteo, BrightSky, Met Norway (swappable) |
| Auth | Laravel built-in + custom customer guard for portal |
| Updates | Signed packages from update server with libsodium verification |
Most of these components are swappable via a registry — which is exactly the lever that modules use. The relevant registries are detailed below and fully documented in Registries.
Dual-brand model
Schneespur and Wintertrace are the same codebase. The brand is determined at install time:
- German locale → Schneespur
- Any other locale → Wintertrace
The brand is stored in the app_brand setting and surfaced via two global helpers:
brand_slug(); // 'schneespur' or 'wintertrace'
brand(); // 'Schneespur' or 'Wintertrace'
Translation files run through a BrandedTranslator that substitutes brand names automatically. A module only needs to think about branding if it produces user-visible text containing the product name — in all other cases the translator handles it.
Application directory structure
The structure below shows how the Laravel application is laid out. For module development, the most relevant directories are app/Services/Extension/ (the registries) and modules/ (the installation directory):
schneespur/ ← Laravel application root
├── app/
│ ├── Console/Commands/ ← Artisan commands (ModulesList, ModulesSync, etc.)
│ ├── Enums/ ← UserRole, JobType, WeatherMoment
│ ├── Events/ ← Domain events (JobCompleted, CustomerCreated, etc.)
│ ├── Exceptions/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Admin/ ← 38 admin controllers
│ │ │ ├── Auth/ ← Authentication controllers
│ │ │ ├── Driver/ ← Driver-facing controllers
│ │ │ ├── Portal/ ← Customer portal controllers
│ │ │ └── Api/ ← OwnTracks API
│ │ ├── Middleware/ ← Auth guards, role enforcement
│ │ └── Requests/ ← Form validation requests
│ ├── Jobs/ ← Queue jobs (FetchWeather, SendCustomerReportEmail)
│ ├── Listeners/ ← Event listeners
│ ├── Mail/ ← Mailable classes
│ ├── Models/ ← 21 Eloquent models
│ ├── Policies/ ← Authorization policies
│ ├── Providers/ ← AppServiceProvider (core registry setup)
│ └── Services/ ← Business logic services
│ └── Extension/ ← ALL extension registries (core of module system)
├── config/
│ └── schneespur_modules.php ← Module system configuration
├── modules/ ← Module installation directory
│ └── example/ ← Bundled reference module
├── resources/
│ ├── views/layouts/ ← admin.blade.php, driver.blade.php, portal.blade.php
│ └── lang/ ← Core translations (de, en)
├── public/
│ └── modules/ ← Symlinks to module dist/ directories
└── storage/
└── app/
└── schneespur_modules_state.json ← Module catalog state
Alongside Services/Extension/, other service directories each bundle an interface with its corresponding registry — for example Weather/, Storage/, Notification/, Pdf/, Scheduler/, Backup/, and Diagnostic/. These interfaces are the contracts a module implements.
Extension architecture
The module system is built on a registry pattern. The core application defines abstract registries and interfaces; modules provide concrete implementations and register them during boot.
┌──────────────────────────────────────────────────┐
│ AppServiceProvider │
│ (registers all registries as singletons, │
│ registers core implementations) │
└──────────────┬───────────────────────────────────┘
│ boot()
▼
┌──────────────────────────────────────────────────┐
│ ModuleManager │
│ discover() → modules/*/module.json │
│ boot() → autoloader → ServiceProvider │
└──────────────┬───────────────────────────────────┘
│ for each enabled module
▼
┌──────────────────────────────────────────────────┐
│ Module ServiceProvider │
│ register() → bind services into container │
│ boot() → register into extension registries │
│ → register routes, views, events │
└──────────────────────────────────────────────────┘
The key principle
Modules never modify core files. They register capabilities into registries during their ServiceProvider’s boot() method. The core application reads from these registries at runtime.
The reason this architecture works: as long as a module only registers and never patches the core, updates remain conflict-free. A new application version can replace core files without breaking modules, and a module can be removed without leaving traces in the core. That is the difference between an extension system and a collection of patches.
Core registries overview
| Registry | Purpose | Interface to implement |
|---|---|---|
NavigationRegistry | Admin sidebar menu items | — (config array) |
DashboardWidgetRegistry | Dashboard cards/widgets | — (config array + Blade view) |
FilterRegistry | Hook/filter pipeline (WordPress-style) | — (callable) |
SlotRegistry | Template injection points | — (Blade view) |
PermissionRegistry | Authorization permissions | — (config array) |
RoleTemplateRegistry | Pre-built role configurations | — (config array) |
DispatchStrategyRegistry | Job assignment algorithms | DispatchStrategyInterface |
NotificationChannelRegistry | Notification transports | NotificationChannelInterface |
TwoFactorMethodRegistry | 2FA implementations | TwoFactorMethodInterface |
BackupTargetRegistry | Backup destinations | BackupTargetInterface |
StorageBackendRegistry | File storage backends | StorageBackendInterface |
WeatherProviderRegistry | Weather data sources | WeatherProviderInterface |
PdfRendererRegistry | PDF generation engines | PdfRendererInterface |
ReportFormatRegistry | Export format handlers | ReportFormatInterface |
ScheduledTaskRegistry | Cron tasks | ScheduledTaskInterface |
DiagnosticReporterRegistry | Error/crash reporting | DiagnosticReporterInterface |
ModuleAssetRegistry | CSS/JS asset loading | — (manifest.json) |
ModuleApiRegistrar | REST API route groups | — (route closure) |
The full API for each registry — with methods and examples — is in the Registries reference. If this is your first module, the fastest path is the Quick start.