Architecture
| Layer | Choice |
|---|---|
| Runtime | PHP 8.5, Laravel 13 |
| Admin and editor panel | Filament 5 on Livewire 4, Filament Shield for permissions |
| Student front-end | Blade and Livewire 4 in a theme, Tailwind 4 on semantic tokens |
| Authentication | Laravel Fortify with our own views, two-factor and passkeys, Laravel Socialite (Google, Facebook) |
| Authorisation | spatie/laravel-permission roles and Action:Entity permissions |
| Data | Eloquent on MySQL 8.0+ or MariaDB 10.11+, Redis as the cache’s and the queue’s first choice — never the site’s only one: sessions live in the database, the cache fails over to it and the queue to running a job after the response (Operations) |
| Files | A media library of our own over spatie/laravel-medialibrary: every file an asset shown by reference, public or course material, on this server or in an S3-compatible bucket (Media) |
| Machine access | laravel/passport for /api/v1 and for agents connecting over MCP (laravel/mcp) |
| Assistants | laravel/ai behind the ai module |
| Operations | Horizon, Pulse, Telescope, spatie health, backup and activitylog |
DB_CONNECTION chooses mysql or mariadb; both read the same DB_* keys, and the suite runs on both before every push (bin/gates, Quality).
Modular monolith
Section titled “Modular monolith”app/ is deliberately thin: AppServiceProvider (the Eloquent guard rails, the morph map and the relocation of two package caches for the host), the Filament panel provider, the shared upload and action components, the kernel of the Settings cluster (app/Filament/Settings, Settings), the hilms:make-module and hilms:make-settings commands and Shield’s RolePolicy. Everything with a domain lives in a module under app-modules/, each a Composer path package with the namespace Hilms\<Module>:
| Module | Owns |
|---|---|
access |
Users, roles and permissions, Fortify, OAuth, passkeys, profile fields, the clock a person keeps, account deletion, the Users resource, the content security policy presets |
languages |
The languages an installation speaks and the clock it keeps, locale-prefixed addresses, translation groups, the phrase editor, the Languages, Phrases and Region pages |
theme |
The theme kernel: manifests, chain resolution, per-theme assets, tokens, styles (what an administrator lays over the tokens), the view contract, the page-template registry, Branding, the design catalogue |
navigation |
Menus: their models, the places they fill, the link sources other modules register, the cached snapshot, the builder |
library |
The media library: assets and where they are used, who sees what, the picker, the addresses of files, moving files between disks |
blocks |
The block kernel, the built-in blocks, the canvas and its settings panel, the patterns, the renderer, the Markdown readers, hilms:make-block |
catalog |
Categories, courses, sections, lessons, their panel resources, the course page and its rail |
pages |
Informational pages built from blocks, the front-door and fallback routes, the destinations an installation points at them |
learning |
Entitlements, enrolments, progress, the course player, the graded quiz, serving course material |
ai |
Editor assistants: settings, providers, agents, queued generations and their review page |
api |
/api/v1 on Passport, the OAuth flow for agents, the MCP authoring server, API clients |
ops |
Horizon, Pulse, Telescope, health, backups, the audit trail, mail and media settings, the installer |
Modules are discovered by internachi/modular. A module’s service provider, routes, views, translations, migrations, factories, policies, listeners, Blade components, Livewire components and console commands register themselves by convention (see Modules).
Module direction
Section titled “Module direction”Dependencies run one way, and a change that reverses an arrow is a design error, not a shortcut:
ops may import everything; nothing imports itai, api build on learning; ai also reaches catalog and blockslearning builds on catalog and blockspages builds on blocks, navigation and accesscatalog builds on blocks and navigationblocks builds on theme and librarylibrary builds on languages and accessnavigation builds on themetheme builds on languageslanguages builds on accessaccess the floor everything stands onEvery arrow points down that list, and no module imports one above it.
The deliberate exceptions, each documented where it happens:
catalogreaches intolearningin exactly two places: the course page renders<x-learning::enrol-button>, andCourseResource::getRelations()mounts learning’s “Students” relation manager.- A module above
catalogadds an action to the course, lesson or page editor throughHilms\Catalog\Filament\EditorActions, registering from its own provider instead of being imported. accesssits belowtheme, sothemedeclares access’s contract views itself (Hilms\Theme\Contract\AccessViews) andHilms\Access\Csp\HotOriginsglobsthemes/*/public/hotrather than importingtheme.accessalso sits belowlanguageswhile keepingusers.localeandusers.timezone, so it asks for the list of languages throughHilms\Access\Contracts\SpokenLanguagesand for the installation’s clock throughHilms\Access\Contracts\InstallationTimezone, each with a floor of its own underneath (see Languages and Time and clocks); and a module that writes in a language tellslanguageswhat it holds throughHilms\Languages\Contracts\LanguageUsage, registered from its own provider.accesssends people somewhere after they sign in but sits below the module that keeps the pages, so it asks throughHilms\Access\Contracts\Destinations, whichpagesbinds and the site’s root answers for until it does (see Pages).catalogdraws the course page’s programme the way the player does — open where a student would resume, every lesson a link when the seat grants access, finished lessons ticked — but sits below the module that keeps the seats, so it asks throughHilms\Catalog\Contracts\ReadingPosition:of($course, $user)answers aHilms\Catalog\Support\Reading,catalogbindsNoReadingPosition(nobody is anywhere) as the floor, andlearningbindsEnrollmentReadingPosition(Catalog).catalogasks, the same way, whether a course holds a history it may never purge:Hilms\Catalog\Contracts\CourseHistory, whichlearningbinds (GrantHistory: an entitlement exists, revoked or not) over theNoCourseHistoryfloor.libraryknows no course, so who teaches what reaches it throughHilms\Library\Contracts\TeachingScope, whichcatalogbinds (CourseTeaching) over theNothingTaughtfloor; and a record that shows assets answers for itself throughHilms\Library\Contracts\UsesAssets, while a model that chooses the disk its files live on answersHilms\Library\Contracts\ChoosesMediaDisk(Media).
navigation knows none of the modules above it: a model that may be linked to registers itself with LinkSources from its own provider (Navigation). A page template is registered with the registry in theme, which is how a module draws a page without importing pages.
Nothing imports pages but ops, which is where the one action that needs both the menus and the pages they point at lives (SeedDefaultMenus).
Request flow
Section titled “Request flow”- Student pages are routes in
app-modules/<module>/routes/*.phpinside thewebgroup, handled by small controllers or full-page Livewire components. Each such file wraps its routes inRoute::localize(), so every public address exists behind a language prefix as well as bare (see Languages). They render views the theme owns: the core ships no Blade a student sees (see Theming). - There are few such routes, because most of what a visitor reads is a page of blocks:
/and the one-segment fallback belong topages, and the course page and the player are the only addresses the catalogue and the learning side keep. A course listing and a student’s own courses are blocks an editor put on a page. - The panel is one Filament panel (
admin) declared inapp/Providers/Filament/AdminPanelProvider.php. It discovers nothing itself; each module ships aFilament\<Module>Pluginthat discovers its own resources, pages and widgets, attached by the module provider throughPanel::configureUsing. - Authentication for both worlds is Fortify:
/loginis the single login page and Filament’s own is switched off.User::canAccessPanel()decides who enters/admin, andHilms\Access\Support\LandingUrldecides where a login lands. - Authorisation in the panel goes through Shield-generated policies checking
Action:Entitypermissions. The panel runs withstrictAuthorization(), so a missing policy method denies. Theadminrole bypasses every check throughGate::before. - Machines reach
/api/v1with a Passport client-credentials token; agents reach/mcp/authoringwith a token that names the person who approved it (see API and agents).
Data model in one picture
Section titled “Data model in one picture”users ──< course_instructor >── courses ──< sections ──< lessons │ │ │ │ ├──< social_accounts │ ├── category ├──< content_blocks ├──< passkeys │ ├── tags └──< lesson_completions ├──< entitlements >────────────┤ └── cover_asset ─┐ │ ├──< enrollments >────────────┤ │ │ │ └──────────────────────┼───────────────────┼──────────────┘ └──< quiz_attempts >────────────┘ │ ▼media_assets ──< media (spatie) media_asset_uses: an asset shown by └──< media_asset_uses >── a lesson, a page or a course, in a slot
pages ──< content_blocks branding settingsmenus ──< menu_items >── a page, a course or a category activity_log └──< menu_locations oauth_clientsai_generations ──> a lesson or a courselanguages translation_overridesstyles: per theme, one active by a unique active_themecourses and pages each carry a language and a translation_group: the same course written in two languages is two rows of one group, each with its own sections, lessons and blocks, showing the same library files by reference (see Languages).
A lesson, a page and a course hold no file of their own. A block names a library asset by id and a course names its cover in cover_asset_id; media_asset_uses records where each asset is shown, and that record is both what the renderer draws and what the access check reads (Media).
Five denormalisations worth knowing:
lessons.course_idsits besidesection_id, so a lesson is found inside its course without joining sections and its slug can be unique per course.enrollmentsis a derived seat:entitlementsis the source of truth, and onlySyncEnrollmentAccesswrites the seat’s status and expiry (see Learning).users.profileis one JSON column holding the values of whatever profile fields the installation defined, because a field an installation invents cannot have a migration of its own.categories.nameanddescription,tags.nameandslug, and the twobrandinglines are JSON columns with one value per language, because they are labels rather than content.menu_locations.locationis one string carrying both the area and the language (header.pl), because the package keys a location by one unique string (see Navigation).
Nothing on a pages row says what the application links to: three settings do (see Pages).
Polymorphic types are pinned by a morph map in AppServiceProvider: user, social_account, category, course, section, lesson, page, language, translation_override, content_block, entitlement, enrollment, api_client, branding, style, ai_generation, menu, menu_item, menu_location, media_asset. A new model in a morph relation is added there.
Documents
Section titled “Documents”SPECS.mdis the current specification and is updated in the same change that alters the system.PLAN.mdis the roadmap: batches, decisions, what comes next.CLAUDE.mdholds the working rules for the AI collaborator;AGENTS.mdis generated by Laravel Boost.docs/install/is the operator’s documentation: requirements, Docker, manual install, upgrades, media storage, theming, languages, MCP, retention, social login.- This book and the user book explain; none of the three keeps history.
HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.