Features

Markdown

Project settings

Project settings are the operator-facing configuration for the whole Externa install. From externa-core 1.3.0 (#201), /settings/project is split into dedicated sections (General, Localization, Auth & Security, Content, Files & Media, API, Integrations). Sidebar module order lives under Appearance → Sidebar. They require can-manage-project-settings.

Project settings
Project settings — general

Related

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

URL bookmarks (1.3.0)

Deep links that pointed at a single #section on /settings/project no longer apply. Use the paths in the HTTP table (for example /settings/project/security for SSO, /settings/project/integrations for webhooks/notifiers, /settings/appearance/sidebar for sidebar modules). Deprecated test aliases (project.webhook-test, notifier tests under /settings/project/*-test) stay for one minor — prefer the project.integrations.* names.

Access

ItemDetail
Permissioncan-manage-project-settings
Routesroutes/settings.php (auth + verified + permission:…)
ControllersProject*SettingsController, AppearanceSettingsController, AppearanceSidebarSettingsController
Pagesresources/js/pages/settings/project.tsx, project/*.tsx, appearance.tsx, appearance/sidebar.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.editGeneral (identity + report URLs)
PUT/settings/projectproject.updatePersist General keys
GET/settings/project/localizationproject.localization.editUI language + content locales
PUT/settings/project/localizationproject.localization.updatePersist Localization keys
GET/settings/project/securityproject.security.editAuth, registration, SSO/OIDC
PUT/settings/project/securityproject.security.updatePersist Auth & Security keys
GET/settings/project/contentproject.content.editLive Preview default + revision retention
PUT/settings/project/contentproject.content.updatePersist Content keys
GET/settings/project/filesproject.files.editUpload caps + image transforms
PUT/settings/project/filesproject.files.updatePersist Files & Media keys
GET/settings/project/apiproject.api.editPublic API allowed origins
PUT/settings/project/apiproject.api.updatePersist API keys
GET/settings/project/integrationsproject.integrations.editWebhooks + Slack/Telegram notifiers
PUT/settings/project/integrationsproject.integrations.updatePersist Integrations keys
POST/settings/project/integrations/webhook-testproject.integrations.webhook-testQueue a signed ping event
POST/settings/project/integrations/slack-notifier-testproject.integrations.slack-notifier-testSend a test Slack message
POST/settings/project/integrations/telegram-notifier-testproject.integrations.telegram-notifier-testSend a test Telegram message
POST/settings/project/webhook-testproject.webhook-testDeprecated alias (one minor) → webhook test
POST/settings/project/slack-notifier-testproject.slack-notifier-testDeprecated alias (one minor) → Slack test
POST/settings/project/telegram-notifier-testproject.telegram-notifier-testDeprecated alias (one minor) → Telegram test
POST/settings/project/integrations/deployproject.integrations.deployTrigger deploy hooks (deploy.trigger, 1.4.0)
GET/settings/ai-toolsai-tools.editAI write tools allowlist (1.4.0)
PUT/settings/ai-toolsai-tools.updatePersist ai.write_tools
GET/settings/appearanceappearance.editBranding form
PUT/settings/appearanceappearance.updatePersist appearance keys
GET/settings/appearance/sidebarappearance.sidebar.editSidebar module order / visibility
PUT/settings/appearance/sidebarappearance.sidebar.updatePersist sidebar_modules

Unsaved changes

Each section form shows 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.

Sections and keys

Every writable project setting key appears exactly once below (matched to the merged Form Requests). Write-only secrets never round-trip on Inertia — the UI shows *_configured booleans instead.

General — /settings/project

Request: UpdateProjectSettingsRequest. Page: resources/js/pages/settings/project.tsx.

KeyNotes
nameProject display name
descriptionShort description
urlPublic / marketing site URL
report_issue_url“Report issue” deep link
report_bug_url“Report bug” deep link
report_error_url“Report error” deep link

Localization — /settings/project/localization

Request: UpdateProjectLocalizationSettingsRequest. Page: resources/js/pages/settings/project/localization.tsx.

KeyNotes
default_languageAdmin UI shell locale
content_localesEnabled content languages (source of truth once saved)
default_content_localeDefault content locale
fallback_content_localesOrdered fallback chain for missing translations

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.

Editing Content languages rewrites fallback_content_locales to [default, …other enabled locales in list order] before save — see Locale fallback for the read-time chain (fallback_content_locales → APP_FALLBACK_LOCALE → null) and empty-list normalization.

On the item form, editors switch locales from the toolbar, copy or apply values across locales, and optionally translate with AI — see Translation workspace.

Auth & Security — /settings/project/security

Request: UpdateProjectSecuritySettingsRequest. Page: resources/js/pages/settings/project/security.tsx. Includes SSO/OIDC.

KeyNotes
password_policyweak / medium / strong
login_max_attemptsThrottle window for login
registration_enabledPublic self-registration
default_user_roleRole assigned on register / JIT
email_verification_requiredRequire verified email
two_factor_requiredEnforce MFA (TOTP or passkey)
allowed_domainsEmail domain allowlist (empty = no domain gate)
oidc_enabledShow SSO button on login
oidc_issuerIssuer URL (discovery at /.well-known/openid-configuration)
oidc_client_idOIDC client id
oidc_client_secretWrite-only — encrypted at rest; never shared on Inertia
oidc_button_labelOptional custom button text
oidc_jit_provisioningCreate users on first login (default off)

Full SSO guide: SSO / OIDC.

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.

Content — /settings/project/content

Request: UpdateProjectContentSettingsRequest. Page: resources/js/pages/settings/project/content.tsx. Includes Live Preview default URL.

KeyNotes
preview_url_defaultFallback Live Preview URL when the collection field is empty
revision_retention_countMax revisions to keep (optional)
revision_retention_daysMax age in days (optional)

See Live Preview · Content versioning.

Files & Media — /settings/project/files

Request: UpdateProjectFilesSettingsRequest. Page: resources/js/pages/settings/project/files.tsx.

KeyNotes
files_max_upload_bytesSoft cap for File Manager uploads (null = unlimited aside from PHP)
chat_max_upload_bytesSoft cap for team chat attachments
upload_allowed_extensionsOptional extension allowlist (null/empty = no extra extension filter)
upload_allowed_mimesOptional MIME allowlist (null/empty = config/files.php defaults)
upload_allow_svgExplicit SVG opt-in (false by default; XSS risk when served/inlined)
allowed_transformationsEnabled transform operations
preset_transformationsNamed presets (key, fit, width, height, quality, …)

Named presets, focal point, Public API URLs, caching, and a Next <Image> loader: Image transforms. Disk / MIME / magic-byte rules: Files configuration · File manager — Upload security.

API — /settings/project/api

Request: UpdateProjectApiSettingsRequest. Page: resources/js/pages/settings/project/api.tsx.

KeyNotes
public_api_allowed_originsOne origin per line in the UI; empty = no origin gate (CORS from CORS_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: Origin allowlist.

Integrations — /settings/project/integrations

Request: UpdateProjectIntegrationsSettingsRequest. Page: resources/js/pages/settings/project/integrations.tsx. Webhooks + Slack/Telegram notifiers.

KeyNotes
webhook_urlOutbound webhook endpoint
webhook_secretWrite-only — encrypted at rest; never shared on Inertia
notifier_slack_webhook_urlSlack Incoming Webhook URL
notifier_telegram_bot_tokenWrite-only — encrypted; BotFather digits:token format
notifier_telegram_chat_idTelegram chat / channel id
notifier_event_typesWhich events fan out to notifiers (null / empty choice = all)
deploy_hooksNamed deploy hooks (1.4.0); each URL encrypted at rest
deploy_on_publishTrigger every deploy hook when an item is published (1.4.0)

Guides: Outbound webhooks · Slack & Telegram notifiers.

Notifiers (Slack, Telegram)

Outbound chat notifications for domain events (1.2.0+). They run in parallel with outbound webhooks, which stay the machine-to-machine channel.

  • Event types: notifier_event_types picks which events are sent; an empty choice (null) means all selectable events. ping (test) is always allowed.
  • Deliveries are queued jobs (DeliverSlackOutboundNotifierJob, DeliverTelegramOutboundNotifierJob) dispatched after commit, so a queue worker must be running.
  • Test buttons hit project.integrations.*-notifier-test (deprecated aliases still work for one minor).
  • Messages are formatted per channel (Telegram uses HTML parse mode) with item/collection context.

Deploy hooks

From externa-core 1.4.0 (#196), store build hooks from Vercel, Netlify, Cloudflare Pages, Laravel Forge, or any service that starts a deploy on POST.

ItemDetail
HooksUp to 10, each with a name (≤120 chars) and URL (≤2048). URLs are encrypted and never sent back to the browser
URL checkSame outbound SSRF guard as outbound webhooks (private hosts need WEBHOOKS_ALLOW_PRIVATE_HOSTS=true)
Manual triggerDeploy button in the admin header, for users with deploy.trigger. POST /settings/project/integrations/deploy (optional hook_id)
Deploy on publishdeploy_on_publish fires every hook after an item is published (manual or scheduled publish)
DeliveryQueued DeliverDeployHookJob: POST, 5 s timeout, no redirects, User-Agent: Externa-Deploy/1.0, 3 tries (10 / 30 / 60 s)
AuditActivity log log_name=deploy, event=deploy_triggered (hook ids + source); Integrations shows the last run status

A queue worker must be running. Editing hooks needs can-manage-project-settings; triggering them needs the new deploy.trigger permission, which externa:upgrade grants to the built-in admin role. MCP publish_item fires hooks only after its approval — MCP server.

Webhook test

POST …/integrations/webhook-test (or the deprecated /settings/project/webhook-test) requires a configured URL; otherwise redirects with an error. Dispatches OutboundWebhookDispatcher::dispatchPing() onto the queue (needs a worker). Integrations settings also shows last success / last error and a recent delivery ring buffer after jobs finish.

Appearance (branding) — /settings/appearance

Request: UpdateAppearanceSettingsRequest. Page: resources/js/pages/settings/appearance.tsx. Presets: config/theme-presets.php, ThemePresetService.

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

KeyPurpose
theme_presetBuilt-in theme preset (1.4.0): teal (default), indigo, violet, rose, slate
project_color / project_color_darkOptional custom brand colors (#RRGGBB); override the preset
project_logo / project_logo_darkLogos (form fields project_logo_id / project_logo_dark_id)
public_faviconFavicon (form field public_favicon_id)
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.

Theme presets (1.4.0)

From externa-core 1.4.0 (#243), Settings → Appearance offers five built-in presets with a live preview: qiick Teal (default), Indigo, Violet, Rose, and Slate. Each preset sets the full light and dark token set (config/theme-presets.php).

  • Precedence: custom brand hex → selected preset → default teal. A custom colour is stored only when you set one, and saving Appearance keeps the selected preset (#437).
  • AI accent: AI surfaces use violet. With the Violet preset, or a custom hex within about 30° of the violet hue, the AI tokens switch to teal so AI stays visually distinct.

Brand colour and focus-ring contrast

A custom brand colour is used for the focus ring and focused input border only when it reaches 3:1 against the page background (WCAG 1.4.11 / 2.4.7), checked separately for light and dark. Otherwise Externa keeps the default teal ring (#453). The check uses the page background only; a brand that barely passes there may still be lower on card surfaces. Button text on the brand colour picks a contrasting foreground. See Accessibility.

Transactional email branding

Password reset, email verification, and other Markdown / MailMessage emails use the same project branding:

  • Logo — light project_logo (emails use a light theme). If unset, the header shows the project name as text (never the default Laravel logo).
  • Primary color — project_color for buttons and links (button text uses contrasting black/white).
  • Name — Project settings name when set; otherwise APP_NAME.

The logo must be reachable by email clients as an absolute public URL (assets disk / FILE_PUBLIC_URL_BASE / CDN / S3). Localhost URLs work only for local mail testing; production mailboxes cannot load them.

Request: UpdateAppearanceSidebarSettingsRequest. Page: resources/js/pages/settings/appearance/sidebar.tsx.

KeyNotes
sidebar_modulesOrdered modules (id, enabled, locked). ai / dashboard locked

Moved out of the former monolithic Project page in 1.3.0 so branding and navigation stay under Appearance.

Source map

ConcernLocation
Routesroutes/settings.php
GeneralProjectSettingsController.php + UpdateProjectSettingsRequest.php
LocalizationProjectLocalizationSettingsController.php + UpdateProjectLocalizationSettingsRequest.php
Auth & SecurityProjectSecuritySettingsController.php + UpdateProjectSecuritySettingsRequest.php
ContentProjectContentSettingsController.php + UpdateProjectContentSettingsRequest.php
Files & MediaProjectFilesSettingsController.php + UpdateProjectFilesSettingsRequest.php
APIProjectApiSettingsController.php + UpdateProjectApiSettingsRequest.php
IntegrationsProjectIntegrationsSettingsController.php + UpdateProjectIntegrationsSettingsRequest.php
AppearanceAppearanceSettingsController.php + UpdateAppearanceSettingsRequest.php
SidebarAppearanceSidebarSettingsController.php + UpdateAppearanceSidebarSettingsRequest.php
Config keys / defaultsconfig/settings.php
Settings storeapp/Services/Settings/SettingsRepository.php, ProjectSettings.php, ProjectAppearance.php
UIresources/js/pages/settings/project.tsx, project/*.tsx, appearance.tsx, appearance/sidebar.tsx
Settings navresources/js/layouts/settings/layout.tsx