Concepts
Architecture
externa-core is a Laravel + Inertia React single-page application: Blade only boots the shell, all product UI lives in React pages, and domain logic sits in controllers, form requests, services, and queued jobs.
Scope
This page describes the application architecture inside externa-core. For a directory map, see Project layout. For HTTP surfaces, see Routing overview.
Stack
| Layer | Choice |
|---|---|
| PHP | 8.3+ (development often on 8.4) |
| Framework | Laravel 13 |
| Auth | Laravel Fortify (session web guard) |
| SPA bridge | Inertia Laravel + @inertiajs/react |
| UI | React 19, Vite, TypeScript |
| Route helpers | Laravel Wayfinder (generated resources/js/routes / actions) |
| Permissions | Spatie Permission + EffectivePermissionResolver |
| AI | laravel/ai agent (AppAssistant) |
Web middleware stack (see bootstrap/app.php) appends HandleAppearance, HandleInertiaRequests, and AddLinkHeadersForPreloadedAssets after the default web group.
Frontend entry is resources/js/app.tsx (createInertiaApp, pages resolved as ./pages/${name}.tsx). The root Blade view is resources/views/app.blade.php.
Domain modules
There is no separate domains/ package. Product areas are conventional folders under app/, routes/, and resources/js/, composed from routes/web.php via sibling route files.
Admin
| Layer | Location |
|---|---|
| Routes | routes/admin.php |
| Controllers | app/Http/Controllers/Admin/* |
| Pages | resources/js/pages/admin/{users,groups,roles,permissions,activity-logs,files}/ |
Covers users, groups, roles, permissions, activity logs, and the file manager (mounted under /files/* with can.manage.files middleware). Controllers include UserController, UserGroupController, RoleController, PermissionController, ActivityLogController, and FileController.
Collections
| Layer | Location |
|---|---|
| Routes | routes/collections.php |
| Controllers | app/Http/Controllers/Collections/* |
| Services | app/Services/Collections/* |
| Models | Collection, CollectionField, CollectionItem, CollectionItemValue |
| Pages | resources/js/pages/collections/{collections,items}/ |
Dynamic content schema (EAV). See Collections data model and Field types.
Authorization note
Collections HTTP routes use auth + verified plus Spatie permission:can-*-collections on each action (bulk authorize in FormRequest). Item ACL / item_filter also run through CollectionPermissionEnforcer. Details: Effective permissions · Collections API.
Files
The file manager is part of the admin route file, not a separate routes/files.php. Domain logic lives in FileService, FileTransformService, models File / FileVersion / FileUpload, and jobs for zip/duplicate work. See File tree model.
AI
| Layer | Location |
|---|---|
| Routes | routes/ai.php |
| Controllers | app/Http/Controllers/Ai/* |
| Agent / tools | app/Ai/Agents/AppAssistant.php, app/Ai/Tools/* |
| Pages | resources/js/pages/ai/ |
Chat, conversations, attachments, imports, and sync sources. Routes require permission:can-use-ai (plus tighter checks inside tools). See AI assistant model.
Settings
| Layer | Location |
|---|---|
| Routes | routes/settings.php |
| Controllers | ProfileController, SecurityController, LocaleController, ProjectSettingsController, AppearanceSettingsController |
| Pages | resources/js/pages/settings/{profile,security,project,appearance}.tsx |
Personal profile/security/locale need session auth only. Project and project appearance (branding) require can-manage-project-settings. Personal light/dark theme still uses the appearance cookie via HandleAppearance / useAppearance (user menu), separate from project branding. See Account settings and Project settings.
Shell
- Dashboard:
DashboardController→dashboard - Notifications:
NotificationController - Auth pages: Fortify → Inertia views under
resources/js/pages/auth/*
Controllers → services → jobs
The codebase favors a thin HTTP layer and fat domain services:
- Controller — authorize (where middleware/FormRequest does), call a service, return Inertia or a JSON/resource response.
- Form request — validation and, for admin resources, permission checks via
AuthorizesWithPermission. - Service — business rules and persistence (
app/Services/…). - Job — work that must not block the request (
app/Jobs/…,ShouldQueue).
Actions
PHP Actions exist mainly for Fortify contracts under app/Actions/Fortify/:
CreateNewUserResetUserPassword
Wired in FortifyServiceProvider. Client-side resources/js/actions/ are Wayfinder-generated helpers from Laravel routes, not domain Action classes.
Jobs
| Job | Role |
|---|---|
ImportCollectionJob | CSV / Excel / remote JSON import; cache status; used by AI sync |
DuplicateFilesJob | Bulk / large file copy + user notification |
PrepareFilesZipJob | Build a download zip + notify when ready |
Default queue connection is config/queue.php → env('QUEUE_CONNECTION', 'database'). Local composer dev typically runs queue:listen alongside the HTTP server and Vite.
Services (examples)
| Service | Role |
|---|---|
Authorization\EffectivePermissionResolver | Effective roles/permissions for Inertia and middleware |
FileService | File tree CRUD, uploads, attach/detach, tags, favorites, zips |
FileTransformService | Image thumbnails |
Collections\CollectionItemValuesWriter | Persist EAV value rows |
Collections\CollectionItemValuesAssembler | Rebuild item data from value rows |
Collections\CollectionItemDataNormalizer | Coerce payloads by field type |
Collections\CollectionItemDataRuleBuilder | Build Laravel validation rules for item data |
AI capabilities are tool classes under app/Ai/Tools, not Services.
Queue and schedule
There is no classic app/Console/Kernel.php. The schedule lives in routes/console.php (Laravel 11+ style).
| Cadence | Command | Purpose |
|---|---|---|
daily() | activitylog:clean | Spatie activity log retention |
hourly() | files:cleanup-uploads | Stale chunked upload sessions |
hourly() | files:cleanup-zips | Expired prepared zip archives |
daily() | ai:cleanup-attachments | Expired AI chat attachments |
everyMinute() | ai:run-sync-sources | Due AI sync sources → ImportCollectionJob |
Unscheduled but important: permissions:sync (upsert DB permissions from PermissionEnum and regenerate the frontend enum).
Operations
Worker and scheduler runbooks live under Queues & scheduler.
Shared Inertia props
App\Http\Middleware\HandleInertiaRequests::share exposes props to every page. Frontend types live in resources/js/types/global.d.ts / auth.ts.
| Key | Contents |
|---|---|
errors | Validation / Inertia error bag (from parent share) |
name | config('app.name') |
auth.user | Authenticated user or null |
auth.permissions | Effective permission name list (string[]) |
auth.roleNames | Effective role names (string[]) |
auth.isSuperAdmin | Whether the user holds super-admin (direct or via group) |
sidebarOpen | From cookie sidebar_state (defaults open when missing) |
collectionLocales | Project settings content locales (seeded from config/collections.php) |
collectionLocaleMeta | Catalog metadata for enabled content locales |
defaultContentLocale | Default content locale from project settings |
notifications | { unread_count: number } |
HandleAppearance applies the personal theme cookie for Blade; it is not an Inertia shared prop. Project branding is loaded separately for the SPA — see Project settings.
// Typical page usage
const { auth, collectionLocales } = usePage().props
Permission checks in React go through useCan() reading auth.permissions / auth.isSuperAdmin — see Effective permissions.
Request flow (mental model)
Browser (React / Inertia)
│
▼
routes/{admin,collections,ai,settings}.php
│
▼
Middleware (auth, verified, permission:, can.manage.files, …)
│
▼
Controller + FormRequest
│
├──► Service (sync work)
│
└──► Job (async work)
│
▼
Queue worker
Related pages
- Project layout — where files live
- Effective permissions — authz model
- Collections data model — EAV schema
- File tree model — files domain
- AI assistant model — agent and tools