Skip to main content
Wintertrace
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

LayerTechnology
FrameworkLaravel 11+ (PHP 8.2+)
DatabaseMySQL / MariaDB (production). SQLite :memory: only for the test suite
FrontendBlade templates, Tailwind CSS, Alpine.js
PDFDomPDF (swappable via PdfRendererRegistry)
GPSOwnTracks integration (MQTT/HTTP)
WeatherOpenMeteo, BrightSky, Met Norway (swappable)
AuthLaravel built-in + custom customer guard for portal
UpdatesSigned 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

RegistryPurposeInterface to implement
NavigationRegistryAdmin sidebar menu items— (config array)
DashboardWidgetRegistryDashboard cards/widgets— (config array + Blade view)
FilterRegistryHook/filter pipeline (WordPress-style)— (callable)
SlotRegistryTemplate injection points— (Blade view)
PermissionRegistryAuthorization permissions— (config array)
RoleTemplateRegistryPre-built role configurations— (config array)
DispatchStrategyRegistryJob assignment algorithmsDispatchStrategyInterface
NotificationChannelRegistryNotification transportsNotificationChannelInterface
TwoFactorMethodRegistry2FA implementationsTwoFactorMethodInterface
BackupTargetRegistryBackup destinationsBackupTargetInterface
StorageBackendRegistryFile storage backendsStorageBackendInterface
WeatherProviderRegistryWeather data sourcesWeatherProviderInterface
PdfRendererRegistryPDF generation enginesPdfRendererInterface
ReportFormatRegistryExport format handlersReportFormatInterface
ScheduledTaskRegistryCron tasksScheduledTaskInterface
DiagnosticReporterRegistryError/crash reportingDiagnosticReporterInterface
ModuleAssetRegistryCSS/JS asset loading— (manifest.json)
ModuleApiRegistrarREST 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.