# Architecture

Source: https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/project-layout). For HTTP surfaces, see [Routing overview](https://docs.qiick.io/externa/routing).

## Stack

| Layer         | Choice                                                          |
| ------------- | --------------------------------------------------------------- |
| PHP           | 8.3+ (development often on 8.4)                                 |
| Framework     | Laravel 13                                                      |
| Auth          | Laravel Fortify (session `web` guard) + optional OIDC/SSO       |
| 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](https://docs.qiick.io/externa/collections-data-model) and [Field types](https://docs.qiick.io/externa/field-types).

> 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](https://docs.qiick.io/externa/permissions) · [Collections API](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/ai-assistant).

### Team chat

| Layer       | Location                                                     |
| ----------- | ------------------------------------------------------------ |
| Routes      | `routes/chat.php` (+ item aliases in `collections.php`)      |
| Controllers | `app/Http/Controllers/Chat/*`, `ItemChatController`          |
| Services    | `app/Services/Chat/*`                                        |
| Pages / UI  | `resources/js/pages/chat/`, `components/chat/*`, item drawer |

Unified item + DM threads, Echo presence, attachments. Permission `can-show-chat` for the hub. See [Chat (hub & items)](https://docs.qiick.io/externa/chat).

### 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](https://docs.qiick.io/externa/account-settings) and [Project settings](https://docs.qiick.io/externa/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:

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

| 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](https://docs.qiick.io/externa/operations).

## 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](https://docs.qiick.io/externa/project-settings).

```tsx
// Typical page usage
const { auth, collectionLocales } = usePage().props
```

Permission checks in React go through `useCan()` reading `auth.permissions` / `auth.isSuperAdmin` — see [Effective permissions](https://docs.qiick.io/externa/permissions).

## Request flow (mental model)

```text
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](https://docs.qiick.io/externa/project-layout) — where files live
- [Effective permissions](https://docs.qiick.io/externa/permissions) — authz model
- [Collections data model](https://docs.qiick.io/externa/collections-data-model) — EAV schema
- [File tree model](https://docs.qiick.io/externa/file-tree-model) — files domain
- [AI assistant model](https://docs.qiick.io/externa/ai-assistant) — agent and tools
