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

LayerChoice
PHP8.3+ (development often on 8.4)
FrameworkLaravel 13
AuthLaravel Fortify (session web guard)
SPA bridgeInertia Laravel + @inertiajs/react
UIReact 19, Vite, TypeScript
Route helpersLaravel Wayfinder (generated resources/js/routes / actions)
PermissionsSpatie Permission + EffectivePermissionResolver
AIlaravel/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

LayerLocation
Routesroutes/admin.php
Controllersapp/Http/Controllers/Admin/*
Pagesresources/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

LayerLocation
Routesroutes/collections.php
Controllersapp/Http/Controllers/Collections/*
Servicesapp/Services/Collections/*
ModelsCollection, CollectionField, CollectionItem, CollectionItemValue
Pagesresources/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

LayerLocation
Routesroutes/ai.php
Controllersapp/Http/Controllers/Ai/*
Agent / toolsapp/Ai/Agents/AppAssistant.php, app/Ai/Tools/*
Pagesresources/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

LayerLocation
Routesroutes/settings.php
ControllersProfileController, SecurityController, LocaleController, ProjectSettingsController, AppearanceSettingsController
Pagesresources/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: DashboardControllerdashboard
  • 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:

  1. Controller — authorize (where middleware/FormRequest does), call a service, return Inertia or a JSON/resource response.
  2. Form request — validation and, for admin resources, permission checks via AuthorizesWithPermission.
  3. Service — business rules and persistence (app/Services/…).
  4. Job — work that must not block the request (app/Jobs/…, ShouldQueue).

Actions

PHP Actions exist mainly for Fortify contracts under app/Actions/Fortify/:

  • CreateNewUser
  • ResetUserPassword

Wired in FortifyServiceProvider. Client-side resources/js/actions/ are Wayfinder-generated helpers from Laravel routes, not domain Action classes.

Jobs

JobRole
ImportCollectionJobCSV / Excel / remote JSON import; cache status; used by AI sync
DuplicateFilesJobBulk / large file copy + user notification
PrepareFilesZipJobBuild a download zip + notify when ready

Default queue connection is config/queue.phpenv('QUEUE_CONNECTION', 'database'). Local composer dev typically runs queue:listen alongside the HTTP server and Vite.

Services (examples)

ServiceRole
Authorization\EffectivePermissionResolverEffective roles/permissions for Inertia and middleware
FileServiceFile tree CRUD, uploads, attach/detach, tags, favorites, zips
FileTransformServiceImage thumbnails
Collections\CollectionItemValuesWriterPersist EAV value rows
Collections\CollectionItemValuesAssemblerRebuild item data from value rows
Collections\CollectionItemDataNormalizerCoerce payloads by field type
Collections\CollectionItemDataRuleBuilderBuild 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).

CadenceCommandPurpose
daily()activitylog:cleanSpatie activity log retention
hourly()files:cleanup-uploadsStale chunked upload sessions
hourly()files:cleanup-zipsExpired prepared zip archives
daily()ai:cleanup-attachmentsExpired AI chat attachments
everyMinute()ai:run-sync-sourcesDue 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.

KeyContents
errorsValidation / Inertia error bag (from parent share)
nameconfig('app.name')
auth.userAuthenticated user or null
auth.permissionsEffective permission name list (string[])
auth.roleNamesEffective role names (string[])
auth.isSuperAdminWhether the user holds super-admin (direct or via group)
sidebarOpenFrom cookie sidebar_state (defaults open when missing)
collectionLocalesProject settings content locales (seeded from config/collections.php)
collectionLocaleMetaCatalog metadata for enabled content locales
defaultContentLocaleDefault 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
Previous
Environment variables