Features

Project settings

Project settings are the operator-facing configuration for the whole Externa install: content languages, sidebar modules, security/registration defaults, Public API origin allowlist, image transform presets, outbound webhooks, and branding. They require can-manage-project-settings.

Related

Personal profile/password/passkeys: Account settings · Passkeys. Content locale resolution: Configuration · Collections data model. Public API origins: Client types — Origin allowlist. Outbound delivery: Outbound webhooks. HTTP overview: Routing.

Access

ItemDetail
Permissioncan-manage-project-settings
Routesroutes/settings.php (auth + verified + permission:…)
ControllersProjectSettingsController, AppearanceSettingsController
Pagesresources/js/pages/settings/project.tsx, appearance.tsx
StorageSettingsRepository project scope (config/settings.php keys)

The same can-manage-project-settings permission also grants access to Settings → Performance (/settings/performance) — cache ops status and targeted flush actions. See Operations.

HTTP

MethodPathNamePurpose
GET/settings/projectproject.editProject settings form
PUT/settings/projectproject.updatePersist project keys
POST/settings/project/webhook-testproject.webhook-testQueue a signed ping event
GET/settings/appearanceappearance.editBranding form
PUT/settings/appearanceappearance.updatePersist appearance keys

Unsaved changes

Project and Appearance forms show an Unsaved badge and Discard on the sticky Save bar while dirty. Discard confirms, resets fields to the last loaded values, and stays on the page. Leaving via navigation while dirty uses the same Keep editing / Discard dialog.

Project keys

Configured under Settings → Project (ProjectSettings::forEdit()). Important groups:

AreaKeys (selection)
Identityname, description, url, default_language
Content localescontent_locales, default_content_locale, fallback_content_locales
Sidebarsidebar_modules (ordered modules; ai / dashboard locked)
Security / registrationpassword_policy, login_max_attempts, two_factor_required, registration_enabled, default_user_role, email_verification_required, allowed_domains
Public APIpublic_api_allowed_origins (one origin per line in the UI; empty = no origin gate, CORS from CORS_ALLOWED_ORIGINS)
Filesallowed_transformations, preset_transformations
Reportingreport_issue_url, report_bug_url, report_error_url
Webhookswebhook_url, webhook_secret (encrypted at rest; never shared on Inertia)

Content locales are the source of truth for translatable collection field values and labels once saved. Until then, seed/fallback comes from config/collections.php. Catalog codes live in resources/data/content-locales-catalog.json (ContentLocaleCatalog).

Disabling a content locale does not delete existing translation rows.

Require two-factor authentication

When two_factor_required is enabled, EnsureTwoFactorIsEnabled (web middleware) redirects authenticated users who have neither confirmed Fortify TOTP (hasEnabledTwoFactorAuthentication()) nor at least one registered passkey (hasPasskeysEnabled()) to Settings → Security (security.edit). That page passes twoFactorRequired / twoFactorEnforcedForUser and shows an alert banner until the user finishes enrollment (TOTP or a passkey).

Satisfies the gate?Condition
YesConfirmed Fortify TOTP
Yes≥1 registered passkey (Features::canManagePasskeys() and hasPasskeysEnabled())
YesBoth of the above
NoNeither — forced to Security with the required-MFA banner
N/A (gate off)two_factor_required off — personal TOTP/passkeys stay optional
N/A (no-op)Fortify two-factor feature disabled — middleware does not enforce

Allowlisted so enrollment works: security/password/profile/locale settings, Fortify 2FA + passkey registration/store/destroy + password-confirm routes, and logout. Deleting a user’s last passkey while TOTP is off and the setting is on returns them to the enrollment gate.

Passkey register / passwordless login / RP config: Passkeys. Pre-release checks: Security checklist.

Public API allowed origins

When public_api_allowed_origins is non-empty, Externa:

  • Sets CORS allowed_origins for api/* to that list
  • Enforces EnforcePublicApiOrigin on REST and GraphQL (matching Origin, or a valid Bearer API key when Origin is absent)

Format: https://host or http://host[:port] — no path. Details and security limits: Origin allowlist.

Project appearance (branding)

Settings → Appearance is project branding, not the personal light/dark toggle:

KeyPurpose
project_color / project_color_darkBrand colors
project_logo / project_logo_darkLogos
public_faviconFavicon
default_appearanceDefault theme hint (system / light / dark)

Personal light/dark preference still uses the appearance cookie + useAppearance / AppearanceTabs in the user menu (HandleAppearance middleware). That path has no Spatie permission gate.

Webhook test

POST /settings/project/webhook-test requires a configured URL; otherwise redirects with an error. Dispatches OutboundWebhookDispatcher::dispatchPing() onto the queue (needs a worker). For configure / HMAC / try steps (webhook.site, Bruno helper placeholder), see Outbound webhooks.

Source map

ConcernLocation
Routesroutes/settings.php
Project controllerapp/Http/Controllers/Settings/ProjectSettingsController.php
Appearance controllerapp/Http/Controllers/Settings/AppearanceSettingsController.php
Config keys / defaultsconfig/settings.php
Settings storeapp/Services/Settings/SettingsRepository.php, ProjectSettings.php, ProjectAppearance.php
UIresources/js/pages/settings/project.tsx, appearance.tsx
Previous
Accessibility