Configuration
MarkdownConfiguration
Externa’s runtime settings live in externa-core/config/*.php and are driven by .env (see Environment variables). This page covers the application defaults you should understand before going to production.
Related pages
Files: Files configuration. AI: AI configuration. Permissions model: Effective 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. |
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. |
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
Three locale systems
- Admin UI locale —
config/i18n.php+ user preference / cookie (APP_LOCALEis the default). Controls the shell language (en/it/de). - Content locales — project settings (
content_locales,default_content_locale,fallback_content_locales). Source of truth for translatable collection field values and labels. Seeded fromconfig/collections.phpuntil saved. UI: Project settings. - 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), 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
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.
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).
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 |
SESSION_DRIVER=database
SESSION_LIFETIME=120
CACHE_STORE=database
QUEUE_CONNECTION=database
FILESYSTEM_DISK=local
Queue worker required
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 and 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:
PermissionSeeder→php artisan permissions:syncRoleSeeder→ built-in roles (super-admin,admin,reader,public) and permission assignmentsCreateSuperAdminSeeder→ upserts the user by email and assigns the super-admin role
Production
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→truedisplay_permission_in_exception/display_role_in_exception→false(avoid leaking names)- Wildcard permissions → disabled
Full authorization walkthrough: Effective permissions.
After deploying enum changes, run:
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).
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).
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):
- The requested (or resolved) locale
- Each entry in
fallback_content_localesthat is still enabled (duplicates skipped) APP_FALLBACK_LOCALE(config('app.fallback_locale'), defaulten) if that code is enabled and not already in the chain- If every candidate is missing → the field is
nullin 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.
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), 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 exampleAWS_URLorAZURE_STORAGE_URL) is added toimg-srcandmedia-src(#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. If you embed the admin in an iframe on purpose, change both SECURITY_X_FRAME_OPTIONS and the CSP (frame-ancestors) via SECURITY_CSP.
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
- Update
.env(and secrets store in production). php artisan config:clearor rebuildconfig:cachein production.- If permissions/enums changed:
permissions:sync(+ seed roles if needed). - If content locales changed: smoke-test item forms and API
?locale=for each enabled locale. - Confirm
APP_URLmatches the public host (affects storage URLs and redirects).