--- title: "Livewire Kit Architecture" description: "How the Livewire kit is organised: backend-only modules with Livewire components, views in resources/views, module registration, permission checks and translations." --- # Architecture The Livewire kit splits the work in two. Modules in `Modules/` hold PHP only (components, services, Form Requests, routes), and every Blade view lives in `resources/views`. For the shared backend pieces (tenancy, auth, roles), see [Core architecture](/docs/core/architecture). ## Overview A request goes through a Livewire component in a module and comes out as a Blade view wrapped in a layout: ```text Route::livewire('users', Index::class) → Modules/User/Livewire/Index.php (mount, actions, render) → Service / Repository (queries, transactions) → view('pages.users.index') → resources/views/pages/users/index.blade.php → layouts::app (sidebar or header, picked from the user's layout settings) ``` After the first page load, clicks, form submits and `wire:model` updates go back to the same component over Livewire's own requests. Links use `wire:navigate`, so moving between pages swaps the content without a full reload. A component stays thin. It validates through a Form Request, checks a permission, calls a service and sets UI state (a toast, a redirect, a modal). Queries and transactions belong in the module's `Services` or `Repositories`. ```php public function save(UserService $userService): void { $user = $this->userId ? User::query()->findOrFail($this->userId) : null; $validated = $this->validateFormRequest( UserRequest::class, $this->only(['name', 'email', 'role', 'password', 'password_confirmation', 'send_invitation']), ['user' => $user], ); $user ? $userService->updateUser($user, $validated) : $userService->createUser($validated); Flux::modal('user-form')->close(); Flux::toast(variant: 'success', text: __('modules/user.toasts.updated')); $this->dispatch('user-saved'); } ``` There's more on forms, events and modals in [Livewire](/docs/livewire/livewire). ## Folder map ### Modules (backend only) | Folder | Purpose | | --- | --- | | `Modules//Livewire` | Full-page and nested components (class-based) | | `Modules//Livewire/Forms` | Livewire Form objects, e.g. `Tenant/Livewire/Forms/TenantForm.php` | | `Modules//Livewire/Concerns` | Traits shared by the module's components, e.g. `Tenant/Livewire/Concerns/ManagesDomains.php` | | `Modules//Http/Requests` | Form Requests used for every write | | `Modules//Services`, `Repositories` | Business logic, queries and transactions | | `Modules//Enums`, `Notifications`, `Jobs`, `Rules` | The rest of the module's backend code | | `Modules//routes` | `web.php` and, where needed, `tenant.php` | | `Modules//Providers` | One provider per module, extending `App\Providers\ModuleServiceProvider` | The kit ships these modules: `Auth`, `Dashboard`, `Menu`, `Notification`, `RolePermission`, `Settings`, `Tenant` and `User`. ### Views and assets | Folder | Purpose | | --- | --- | | `resources/views/pages/` | Page views: `auth/`, `notifications/`, `roles/`, `settings/`, `setup/`, `tenants/`, `users/`, plus `dashboard.blade.php` and `home.blade.php` | | `resources/views/pages//partials` | Page-only modals and Blade components, e.g. `users/partials/user-form-modal.blade.php` | | `resources/views/livewire` | Views of shared Livewire widgets: `notification-bell`, `language-switcher`, `delete-account`, `passkeys`, two-factor | | `resources/views/layouts` | `app`, `auth`, `base`, `settings`, plus `app/` (sidebar, header, partials) and `auth/` (card, simple, split) | | `resources/views/components/common` | The `x-common.*` UI wrappers around Flux | | `resources/views/components` | Kit-wide Blade components: `x-page-heading`, `x-table-card`, `x-stat-card`, `x-breadcrumbs`, … | | `resources/views/errors` | 403, 404, 500 and 503 pages | | `resources/views/flux` | Published Flux overrides (a few icons and `navlist/group`) | | `resources/js` | `app.js`, `permission-picker.js`, `passkeys.js` | | `resources/css/app.css` | Tailwind 4, Flux styles and the theme tokens | The views mirror the Vue kit's `resources/js/pages`, so `pages/users/index.blade.php` here is `pages/users/Index.vue` there. ### `app/` | File | Purpose | | --- | --- | | `app/Providers/ModuleServiceProvider.php` | Base provider that loads module routes and registers the module's Livewire namespace | | `app/Providers/AppServiceProvider.php` | Registers the `pages::` Blade namespace, the super admin `Gate::before` and Livewire's persistent middleware | | `app/Livewire/Concerns/ValidatesFormRequests.php` | `validateFormRequest()` | | `app/Livewire/Concerns/AuthorizesPermissions.php` | `authorizePermission()` | | `app/Livewire/Concerns/ThrottlesActions.php` | `throttled()` | We don't write code comments; that's a project rule in `.ai/rules/general.md`. ## Module registration Every module has a provider that extends `ModuleServiceProvider`. Its `boot()` does two things: 1. Loads `routes/web.php` and `routes/tenant.php` from the module, if they exist, inside the `web` middleware group. 2. Registers the module's `Livewire` folder as a Livewire namespace named after the module in kebab-case. ```php protected function registerLivewireComponents(): void { if (is_dir($this->modulePath('Livewire'))) { Livewire::addNamespace($this->moduleName(), classNamespace: Str::beforeLast(static::class, '\\Providers\\').'\\Livewire'); } } ``` That's why tags carry the module name. `Modules\Tenant\Livewire\EditTenantModal` is ``, and `Modules\RolePermission\Livewire\RoleFormModal` is ``. Providers are listed in `bootstrap/providers.php`. A provider can still add its own `register()` or `boot()` code. `TenantServiceProvider` binds `TenantAuthFeatureService` as a singleton, and `AuthServiceProvider` points Fortify at the views in `pages/auth`. ::: tip Most providers are empty classes, e.g. `class DashboardServiceProvider extends ModuleServiceProvider {}`. Routes and components are picked up for you. ::: ## Blade namespaces | Prefix | Points at | Example | | --- | --- | --- | | `x-common.*` | `resources/views/components/common` | `` | | `x-*` | `resources/views/components` | `` | | `x-layouts::*` | `resources/views/layouts` (Livewire's default namespace) | `` | | `x-pages::*` | `resources/views/pages` (registered in `AppServiceProvider`) | `` | | `livewire:::*` | `Modules//Livewire` | `` | ```php Blade::anonymousComponentPath(resource_path('views/pages'), 'pages'); ``` ## Data in the layouts There are no shared props. A component passes its own data to its view from `render()`, and the layouts fetch what they need from services: | Data | Where it comes from | | --- | --- | | Current user | `auth()->user()` | | Layout settings | `@inject('layoutService', 'Modules\Settings\Services\LayoutService')` in `layouts/app.blade.php` and `layouts/auth.blade.php` | | Sidebar and header menus | `@inject('menuService', 'Modules\Menu\Services\MenuService')` in `layouts/app/partials/sidebar-menu.blade.php` and `navbar.blade.php` (`getTree()`, `getSetupTree()`) | | Languages | `` in the header | | Unread notifications | `` in the header | The menu items come back already filtered by permission and marked `active`. How they're built is covered in [Navigation & layouts](/docs/core/navigation-and-layouts). ## Permissions on the frontend Permissions are checked in three places. **Route middleware** protects page access, as in every kit: ```php Route::livewire('users', Index::class) ->middleware('permission:View Users|View Tenant Users') ->name('users.index'); ``` `AppServiceProvider` adds the auth, central-only and permission middleware to Livewire's persistent middleware. Livewire re-applies them on every later request a page makes, so a click on a page you can no longer access fails as well. **Blade directives** hide buttons the user can't use. Pass both the central and the tenant permission name, so the same check works on every domain: ```blade @canany(['Create User', 'Create Tenant User']) {{ __('modules/user.index.add_user') }} @endcanany ``` **`authorizePermission()`** guards actions that need a different permission from the page itself. It aborts with a 403 unless the user has **any** of the names you pass: ```php public function confirmDelete(int $userId): void { $this->authorizePermission('Delete User', 'Delete Tenant User'); $this->deletingUserId = $userId; Flux::modal('delete-user')->show(); } ``` Super admins pass every check through `Gate::before` in `AppServiceProvider`. ::: warning Hiding a button doesn't secure anything, because anyone can call a public component method from the browser. Every action that changes data needs `authorizePermission()` (or a route permission that covers it). See [Users, roles & permissions](/docs/core/users-roles-permissions). ::: ## Translations Translations are plain Laravel PHP files. Blade and PHP use the same slash keys, and nothing has to be generated: ```text lang//modules/.php → __('modules/.') ``` ```blade ``` ```php Flux::toast(variant: 'success', text: __('modules/user.toasts.deleted')); ``` Breadcrumbs, page titles, toasts and confirm dialogs all go through `__()`. Language resolution and adding a language are covered in [Localization](/docs/core/localization). ## Related files - `app/Providers/ModuleServiceProvider.php`: module routes and Livewire namespaces - `app/Providers/AppServiceProvider.php`: `pages::` namespace, `Gate::before`, persistent middleware - `app/Livewire/Concerns/*.php`: `validateFormRequest()`, `authorizePermission()`, `throttled()` - `bootstrap/providers.php`: the list of module providers - `bootstrap/app.php`: middleware aliases (`auth`, `guest`, `central.only`, `permission`, `role`) - `resources/views/layouts/app.blade.php`: the app layout every full-page component renders into - `.ai/rules/livewire.md`: the kit's conventions in one page