# Configuration

Source: https://docs.externa.qiick.io/docs/configuration

Externa’s runtime settings live in `externa-core/config/*.php` and are driven by `.env` (see [Environment variables](/docs/environment-variables)). This page covers the application defaults you should understand before going to production.

> **Related pages**
> Files: [Files configuration](/docs/files-configuration). AI: [AI configuration](/docs/ai-configuration). Permissions model: [Effective permissions](/docs/permissions).

## Application (`config/app.php`)

| Key / env             | Default                                                 | Notes                                                                                                                                                                                                                  |
| --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_NAME`            | `Laravel`                                               | Display name in mail/UI strings that read `config('app.name')`. Shared to Inertia as `name`.                                                                                                                           |
| `APP_ENV`             | `production` in config; `.env.example` uses `local`     | Environment label.                                                                                                                                                                                                     |
| `APP_DEBUG`           | `false` in config; `true` in `.env.example`             | **Disable in production.**                                                                                                                                                                                             |
| `APP_URL`             | `http://localhost`                                      | Canonical URL for Artisan, mail links, public disk URLs, and Fortify passkeys RP. Must match how browsers reach the app. App works on HTTP; passkeys need HTTPS / localhost — [Passkeys](/docs/passkeys#requirements). |
| `APP_KEY`             | _(required)_                                            | Encryption key; generated by `composer setup` / `key:generate`.                                                                                                                                                        |
| `APP_LOCALE`          | `en`                                                    | Default Laravel locale.                                                                                                                                                                                                |
| `APP_FALLBACK_LOCALE` | `en`                                                    | Fallback when a translation key is missing.                                                                                                                                                                            |
| `APP_FAKER_LOCALE`    | `en_US`                                                 | Faker locale for factories/seed demos.                                                                                                                                                                                 |
| `timezone`            | `UTC`                                                   | Hard-coded in config (not env-driven).                                                                                                                                                                                 |
| Maintenance           | `APP_MAINTENANCE_DRIVER=file`, store default `database` | Standard Laravel maintenance mode.                                                                                                                                                                                     |

```env
APP_NAME=Laravel
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
```

> 1. **Admin UI locale** — `config/i18n.php` + user preference / cookie (`APP_LOCALE` is the default). Controls the shell language (en/it/de).
> 2. **Content locales** — **project settings** (`content_locales`, `default_content_locale`, `fallback_content_locales`). Source of truth for translatable collection field values and labels. Seeded from `config/collections.php` until saved. UI: [Project settings](/docs/project-settings).
> 3. **Laravel `APP_LOCALE` / `APP_FALLBACK_LOCALE`** — framework translator defaults; also used as a last-resort content resolve hint when nothing else matches.
> 
> Changing the admin UI language does **not** change which content locales are enabled.

From externa-core **1.4.0** ([#282](https://github.com/qiick-io/externa-core/issues/282)), server-side strings (flash messages, validation and `abort` messages wrapped in `__()`) are also translated: `lang/it.json` and `lang/de.json`, with English source strings as keys and a Pest key-parity test. They follow the admin UI locale. Tool errors and AI machine messages stay English.

## Fortify (`config/fortify.php`)

Laravel Fortify provides session authentication for the Inertia SPA.

| Setting                | Value                                                                   |
| ---------------------- | ----------------------------------------------------------------------- |
| Guard                  | `web`                                                                   |
| Password broker        | `users`                                                                 |
| Username / email field | `email`                                                                 |
| `lowercase_usernames`  | `true`                                                                  |
| Home path              | `/dashboard`                                                            |
| Route prefix / domain  | empty / `null`                                                          |
| Middleware             | `['web']`                                                               |
| Views                  | enabled (`true`) — Inertia auth pages under `resources/js/pages/auth/*` |
| Login throttle         | limiter `login` (5/min)                                                 |
| Two-factor throttle    | limiter `two-factor` (5/min)                                            |
| Passkeys throttle      | limiter `passkeys`                                                      |

### Enabled features

```php
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::twoFactorAuthentication([
    'confirm' => true,
    'confirmPassword' => true,
    'window' => 1, // ±30s TOTP skew
]),
Features::passkeys([
    'confirmPassword' => true,
]),
```

Passkeys also use the `passkeys` config block (`relying_party_id`, `allowed_origins`, `user_handle_secret`, `timeout`) derived from `APP_URL` / `APP_KEY`. Operator guide: [Passkeys](/docs/passkeys).

To disable a feature, remove it from the `features` array in `config/fortify.php` and adjust the UI accordingly.

Authenticated product routes also require the `verified` middleware (see [Routing overview](/docs/routing)).

## Session, cache, and queue defaults

Externa’s `.env.example` uses **database** drivers for all three. That keeps local setup simple and matches the queue-backed file zip/duplicate and AI import jobs.

| Concern                 | Config file              | Env                | Default       |
| ----------------------- | ------------------------ | ------------------ | ------------- |
| Session driver          | `config/session.php`     | `SESSION_DRIVER`   | `database`    |
| Session lifetime        |                          | `SESSION_LIFETIME` | `120` minutes |
| Session encrypt         |                          | `SESSION_ENCRYPT`  | `false`       |
| Cache store             | `config/cache.php`       | `CACHE_STORE`      | `database`    |
| Queue connection        | `config/queue.php`       | `QUEUE_CONNECTION` | `database`    |
| Default filesystem disk | `config/filesystems.php` | `FILESYSTEM_DISK`  | `local`       |

```env
SESSION_DRIVER=database
SESSION_LIFETIME=120
CACHE_STORE=database
QUEUE_CONNECTION=database
FILESYSTEM_DISK=local
```

> With `QUEUE_CONNECTION=database` (or Redis/etc.), you must run a worker. `composer run dev` starts `php artisan queue:listen`. Production needs a supervised worker plus the scheduler — see [Operations](/docs/operations) and [Deployment](/docs/deployment).

Optional Redis variables are present in `.env.example` if you switch drivers later (`REDIS_*`, `MEMCACHED_HOST`).

## Super-admin seeding (`config/super_admin.php`)

Used only by `CreateSuperAdminSeeder` when you run `php artisan db:seed` (via `DatabaseSeeder`).

| Config key   | Env                              | Default                  |
| ------------ | -------------------------------- | ------------------------ |
| `first_name` | `INITIAL_SUPER_ADMIN_FIRST_NAME` | `Super`                  |
| `last_name`  | `INITIAL_SUPER_ADMIN_LAST_NAME`  | `Admin`                  |
| `email`      | `INITIAL_SUPER_ADMIN_EMAIL`      | `superadmin@example.com` |
| `password`   | `INITIAL_SUPER_ADMIN_PASSWORD`   | `password`               |

`DatabaseSeeder` order:

1. `PermissionSeeder` → `php artisan permissions:sync`
2. `RoleSeeder` → built-in roles (`super-admin`, `admin`, `reader`, **`public`**) and permission assignments
3. `CreateSuperAdminSeeder` → upserts the user by email and assigns the super-admin role

> Override `INITIAL_SUPER_ADMIN_*` **before** seeding shared or production environments. Never leave the default password outside local development.

## Permission configuration overview

| Piece                      | Location                                                                         |
| -------------------------- | -------------------------------------------------------------------------------- |
| Spatie package config      | `config/permission.php`                                                          |
| Canonical permission names | `App\Enums\PermissionEnum`                                                       |
| Sync command               | `php artisan permissions:sync` (`--prune` removes DB rows no longer in the enum) |
| TS mirror                  | Regenerated at `resources/js/enums/permission-enum.ts`                           |
| Teams                      | **disabled** (`teams => false`)                                                  |
| Permission cache           | 24 hours, key `spatie.permission.cache`, store `default`                         |
| Effective resolution       | `App\Services\Authorization\EffectivePermissionResolver`                         |
| Middleware aliases         | `permission`, `can.manage.files` in `bootstrap/app.php`                          |

Key Spatie settings that matter for Externa:

- Models: default Spatie `Permission` / `Role`
- `register_permission_check_method` → `true`
- `display_permission_in_exception` / `display_role_in_exception` → `false` (avoid leaking names)
- Wildcard permissions → disabled

Full authorization walkthrough: [Effective permissions](/docs/permissions).

After deploying enum changes, run:

```bash
php artisan permissions:sync
# optional: php artisan permissions:sync --prune
```

## Content locales (project settings)

**Source of truth:** Project settings → `content_locales` (ordered, min 1), `default_content_locale`, optional `fallback_content_locales`.

`config/collections.php` (`locales` / `fallback_locales`) is only the **seed / fallback** when settings have not been saved yet (defaults `['en', 'it']`).

| Key                        | Role                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------- |
| `content_locales`          | Enabled BCP-47 (or language-only) codes for item `data` translations and field labels |
| `default_content_locale`   | Must be ∈ `content_locales`; used when no `?locale=` / Accept-Language match          |
| `fallback_content_locales` | Ordered chain when a translation is missing                                           |

Configure under **Settings → Project → Content languages** (searchable catalog + sortable list). Catalog codes/names/flags live in `resources/data/content-locales-catalog.json` (`ContentLocaleCatalog`). Disabling a locale does **not** delete existing translation rows in the DB (orphans stay until rewritten).

Shared to the frontend as Inertia props `collectionLocales`, `collectionLocaleMeta`, and `defaultContentLocale` (see [Routing overview](/docs/routing)).

Admin UI language remains separate (`config/i18n.php`).

### Locale fallback

Two separate steps: pick the **active locale**, then walk a **value fallback** chain when a translatable field has no usable string for that locale. Applies to **top-level** translatable fields only (nested block maps are not flattened — see [Field types](/docs/field-types#pack-block-schema-articles-pages)).

**Active locale** (`CollectionLocaleResolver::resolve`): explicit override / `?locale=` (must be enabled or **422**), then first `Accept-Language` tag that is enabled, then `default_content_locale`, then `APP_LOCALE` if enabled, then the first enabled content locale. Same rules for the Public CMS API and Live Preview (published flatten path).

**Value fallback** (`fallbackChain` → `getTranslated` / `flattenForLocale`):

1. The requested (or resolved) locale
2. Each entry in `fallback_content_locales` that is still enabled (duplicates skipped)
3. `APP_FALLBACK_LOCALE` (`config('app.fallback_locale')`, default `en`) **if that code is enabled** and not already in the chain
4. If every candidate is missing → the field is **`null`** in the response

Empty strings count as missing: `getTranslated` skips a locale slot when the value is `null` or `''`, then tries the next locale in the chain.

```text
content_locales = [en, it, de]
fallback_content_locales = [en, it, de]   # typical after Content languages save
?locale=de
title = { en: "Hello", it: "" }
→ "Hello"   # de missing → it empty → en
```

**Admin rewrite:** editing **Settings → Project → Content languages** resets `fallback_content_locales` in the form to `[default_content_locale, …other enabled locales in list order]`. Saving that section persists that chain (any earlier custom order is replaced). There is no separate reorder UI; the value is submitted as a hidden field. A custom order sent via the project settings update API is kept until Content languages are edited again.

**Empty list on the server:** when normalizing locales, entries not in `content_locales` are dropped. If `fallback_content_locales` ends up empty, `ProjectSettings` rebuilds `[default_content_locale, …seed codes from config/collections.php fallback_locales that are still enabled, …remaining enabled locales]`.

## Security headers

From externa-core **1.4.0** ([#187](https://github.com/qiick-io/externa-core/issues/187)), `SetSecurityHeadersMiddleware` (web group) adds browser defence headers to admin HTML responses. It is **on by default** (`config/security.php`).

| Env                           | Default                                                            | Header                                                   |
| ----------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- |
| `SECURITY_HEADERS_ENABLED`    | `true`                                                             | Turns all of them on/off                                 |
| —                             | `nosniff` (fixed)                                                  | `X-Content-Type-Options`                                 |
| `SECURITY_X_FRAME_OPTIONS`    | `DENY`                                                             | `X-Frame-Options`                                        |
| `SECURITY_REFERRER_POLICY`    | `strict-origin-when-cross-origin`                                  | `Referrer-Policy`                                        |
| `SECURITY_PERMISSIONS_POLICY` | `camera=(), microphone=(self), geolocation=(), payment=(), usb=()` | `Permissions-Policy`                                     |
| `SECURITY_CSP`                | empty = built-in policy                                            | `Content-Security-Policy` (full override, sent verbatim) |

**Skipped:** `api/*`, `graphql`, `api/graphql` (Public API clients never get the admin CSP), and any response that already sets its own `Content-Security-Policy` (sandboxed user content).

**Built-in CSP:** `default-src 'self'`; scripts and styles allow `'self'` plus `'unsafe-inline'` (scripts also `'unsafe-eval'`) for Vite/Inertia; `img-src 'self' data: blob: https:`; `media-src 'self' blob:`; `connect-src 'self' ws: wss: http: https:` (Reverb/Echo and Vite HMR); `worker-src 'self' blob:`; `frame-ancestors 'none'`; `base-uri 'self'`; `form-action 'self'`; `object-src 'none'`. On top of that:

- the origin of the configured files disk `url` (`filesystems.disks.<FILES_DISK>.url`, for example `AWS_URL` or `AZURE_STORAGE_URL`) is added to `img-src` and `media-src` ([#440](https://github.com/qiick-io/externa-core/issues/440));
- in local dev, the Vite dev server origin from `public/hot` (including Herd HTTPS) is added to script, style, font, and image sources.

**Microphone:** `microphone=(self)` keeps AI chat voice input (SpeechRecognition) working. Set `SECURITY_PERMISSIONS_POLICY` with `microphone=()` to turn voice input off.

HSTS is **not** sent by Externa; set it on the reverse proxy — [Reverse proxy cookbook](/docs/reverse-proxy). If you embed the admin in an iframe on purpose, change both `SECURITY_X_FRAME_OPTIONS` and the CSP (`frame-ancestors`) via `SECURITY_CSP`.

```env
SECURITY_HEADERS_ENABLED=true
# SECURITY_X_FRAME_OPTIONS=DENY
# SECURITY_REFERRER_POLICY=strict-origin-when-cross-origin
# SECURITY_PERMISSIONS_POLICY="camera=(), microphone=(self), geolocation=(), payment=(), usb=()"
# SECURITY_CSP=
```

## Mail and logging (brief)

| Area   | Default in `.env.example`                    |
| ------ | -------------------------------------------- |
| Mailer | `MAIL_MAILER=log` (no SMTP required locally) |
| Log    | `LOG_CHANNEL=stack`, `LOG_LEVEL=debug`       |

Configure real SMTP (or another mailer) before relying on password-reset or verification email in non-local environments.

## Checklist after changing config

1. Update `.env` (and secrets store in production).
2. `php artisan config:clear` or rebuild `config:cache` in production.
3. If permissions/enums changed: `permissions:sync` (+ seed roles if needed).
4. If content locales changed: smoke-test item forms and API `?locale=` for each enabled locale.
5. Confirm `APP_URL` matches the public host (affects storage URLs and redirects).
