Architecture Vue
Everything framework-specific lives in resources/js. The Laravel side (modules, tenancy, auth) is described in Core architecture.
Overview
A request goes through a Laravel module controller and ends as a Vue page inside a layout:
Route → Controller → Service → Inertia::render('users/Index', props)
→ pages/users/Index.vue (+ shared props)
→ default layout picked in app.ts (or the page's own)resources/js/app.ts boots the client:
- Page resolution: no
resolvecallback. The@inertiajs/viteplugin invite.config.tsloads pages fromresources/js/pages. - Default layout by page name:
auth/*→AuthLayout,settings/*→AppLayout+settings/Layout.vue, everything else →AppLayout. Details in Layouts. - Title:
"<page title> - <VITE_APP_NAME>". - Theme:
initializeTheme()fromcomposables/useAppearance.tsapplies light, dark or system mode. - Toasts:
initializeFlashToast()fromlib/flashToast.tsturns server flash data into toasts.
View app.ts
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
layout: (name) => {
switch (true) {
case name.startsWith('auth/'):
return AuthLayout;
case name.startsWith('settings/'):
return [AppLayout, SettingsLayout];
default:
return AppLayout;
}
},
progress: { color: '#4B5563' },
});
initializeTheme();
initializeFlashToast();Folder map
| Folder | Purpose |
|---|---|
pages/ | Inertia pages (PascalCase .vue): auth/, errors/, roles/, settings/, setup/, tenants/, users/, plus Dashboard.vue and Home.vue |
layouts/ | AppLayout.vue, AuthLayout.vue, their variants in app/ and auth/, and settings/Layout.vue |
components/common/ | The kit's form kit: CommonInput, CommonSelect, ConfirmDialog, … |
components/ui/ | shadcn-vue primitives (reka-ui) |
components/ | App shell and feature components: AppSidebar, AppHeader, NavMain, UserMenuContent, … |
composables/ | Shared logic as useX.ts |
lib/ | utils.ts (cn, toUrl) and flashToast.ts |
types/ | Hand-written and generated TypeScript types |
lang/ | Generated translation JSON |
routes/, actions/, wayfinder/ | Generated by Wayfinder, git-ignored |
Components use <script setup lang="ts">. ESLint enforces import type for type-only imports, ordered import groups, 1TBS braces and blank lines around control statements. Code comments are not added (project rule in .ai/rules/general.md). Naming rules are in Conventions.
Composables
| File | Exports |
|---|---|
useAppearance.ts | useAppearance(), initializeTheme(), updateTheme() |
useConfirmDialog.ts | useConfirmDialog() → confirm(options), setLoading() |
useCurrentUrl.ts | useCurrentUrl() → isCurrentUrl, isCurrentOrParentUrl, whenCurrentUrl |
useDebounce.ts | useDebounce(source, delay), useDebounceFn(fn, delay) |
useInitials.ts | getInitials(), useInitials() |
useLanguage.ts | useLanguage(), DEFAULT_LANGUAGE_VALUE |
usePermission.ts | usePermission() → can(...permissions) |
useSecret.ts | generateSecret(), useSecret() |
useSlug.ts | slugify(), useSlug() |
useStatusBadge.ts | useStatusBadge() → workspace status labels and badge classes |
useSubdomain.ts | extractSubdomain(), useSubdomain() → formatDomain() |
useTwoFactorAuth.ts | useTwoFactorAuth(): QR code, setup key and recovery codes via useHttp |
users/useAssignUserPermissions.ts | State for the assign-permissions dialog |
Generated files
| Output | Generated by | Import from |
|---|---|---|
| Route and controller functions | Wayfinder Vite plugin (formVariants: true) or php artisan wayfinder:generate --with-form | @/routes/..., @/actions/... |
Types for Data classes and enums in app/ and Modules/ | php artisan typescript:transform | @/types/Modules/<Module>/Data, .../Enums |
| Translation JSON | php artisan erag:generate-lang | Read by vueLang() |
Hand-written page prop types live in types/Tenants/tenants.ts, types/Users/users.ts and types/Roles/roles.ts and are re-exported from @/types. How to call Wayfinder functions: Inertia → Wayfinder routes.
TIP
composer lint runs wayfinder:generate --with-form and typescript:transform before the frontend linters, so generated files never drift from the PHP classes.
Shared props
HandleInertiaRequests shares these props with every page. They are typed as SharedData in types/global.d.ts.
| Prop | Content |
|---|---|
name | App name (per-domain "App name", or the tenant company on tenant domains) |
auth | user, isTenant, permissions, isSuperAdmin |
locale, userLocale, defaultLocale | Active, user-selected and domain default locale |
languages | Selectable languages (LanguageOption[]) |
menus, setupMenus | Sidebar trees (NavItem[]), already filtered by permission |
layout | LayoutSettings: app_layout, sidebar_variant, sidebar_collapsible, auth_layout |
sidebarOpen | Sidebar cookie state |
appUrl, domain | App URL and central domain |
permissionsConfig | Grouped permissions for the permission dialogs (not declared in SharedData) |
types/global.d.ts registers SharedData with Inertia and declares a global PageProps<T>, so usePage() is typed without imports:
const page = usePage<PageProps>();
const user = computed(() => page.props.auth.user);Prefer the composables (usePermission, useLanguage) over reading props by hand.
Permissions on the frontend
The backend already hides menu items the user cannot see. Inside a page, use usePermission() to hide buttons and actions:
const { can } = usePermission();
const canCreate = can('Create User', 'Create Tenant User');can()returnstruewhen the user has any of the given permissions.- Super admins always get
true. - Pass both the central and the tenant permission name so the check works on every domain.
WARNING
Hiding a button is only cosmetic. Routes must still be protected with the permission: middleware on the backend. See Users, roles & permissions.
Translations
lang/<locale>/modules/<feature>.php
→ php artisan erag:generate-lang
→ resources/js/lang/<locale>/modules/<feature>.json
→ __('modules.<feature>.<key>') in Vue<script setup lang="ts">
import { vueLang } from '@erag/lang-sync-inertia/vue';
const { __ } = vueLang();
</script>
<template>
<p>{{ __('modules.user.index.delete_confirm.message', { name: user.name }) }}</p>
</template>vueLang() also returns trans, transChoice and trans_choice. More in Localization.
WARNING
Import from the /vue subpath. The package root also exports the React and Svelte helpers and breaks the build.
Related files
resources/js/app.ts: app entry, default layouts, theme, flash toastsvite.config.ts: Inertia, Wayfinder (formVariants: true), Tailwind and Vue DevTools pluginsapp/Http/Middleware/HandleInertiaRequests.php: shared propsresources/js/types/global.d.ts:SharedDataandPagePropsresources/js/composables/usePermission.ts,useLanguage.tsresources/js/lib/flashToast.ts,lib/utils.tscomponents.json: shadcn-vue configuration