Known Quirks and Legacy Areas

Places where the current code does something you might not expect. Each entry says what happens and what to do about it, so you can plan around it instead of discovering it in production.

Permissions

Some routes check permissions that are never created

The users and widgets screens check view users, delete users, view widgets, edit widgets and delete widgets, but no seeder or migration creates them. Only the super administrator can view a single user, delete users or use the widgets editor; for other roles the widgets screen answers 403 even with manage widgets.

Workaround: create the five permissions on the site (a small migration with Permission::findOrCreate()) and assign them to the roles that need them. See the permissions catalogue.

Bundled modules' admin screens skip the two-factor check

Core admin routes use auth, verified and 2fa. The Booking, TruCommerce and TruPayments admin groups use web, auth and AdminTheme only, so a user who has not completed two-factor sign-in can open them directly. Your own plugin should use the full stack (Stacks to copy).

Two two-factor middlewares

Admin routes use the 2fa alias; the public account area and the catch-all route use App\Http\Middleware\TwoFactorMiddleware. Both rely on App\Services\Authenticator. Treat both as active.

Events

Booking events are declared but never fired

BookingCreated, BookingUpdated, BookingStatusChanged, BookingCheckedIn and BookingPaymentStatusChanged exist and are mapped to a listener, but nothing dispatches them. Booking notifications are sent directly by its NotificationService.

Workaround: observe the Booking model's Eloquent events, or listen to the payment events for payment changes. See Events, jobs and schedules.

Closing a ticket notifies nobody

TicketCloseEvent is fired but has no listener. Add one in a plugin if closing should send a message.

Page builder and content

Deactivating a plugin can break whole pages, not only its blocks

Saved pages contain shortcode tags such as <x-shortcode-testimonials>. When the plugin that provides the tag is disabled, Blade cannot compile the page; the page renderer catches the error and prints the stored HTML unrendered, so every shortcode on that page stops working. Warn site owners to remove a plugin's blocks before deactivating it, and keep shortcode ids stable across versions.

Every shortcode is visible to the AI assistant unless it opts out

The capability catalogue includes all registered shortcodes; ->ai(['enabled' => false]) removes one. The assistant itself has no settings screen in this release. See AI assistant integration.

A page can pin its own menu

A page whose settings contain primary_menu keeps that menu when the site's primary menu changes. Website templates pin menus on many pages. Test menu work on a page without that setting.

Core slug config names a Booking class

config/slugs.php lists Modules\Booking\Models\Service among the models slugs:backfill processes, which is one of the few places the core refers to a module. Booking is protected, so it is always present; add your own models to that list at runtime from your provider.

Settings and configuration

Settings field types cannot be extended

The list of field types is built fresh for every field, with no registration hook. Compose the existing types, use an ajax-button for actions, or build a custom admin page for richer UI. EncryptedPassword and ProviderModelSelect exist as classes but are not in the list.

Saving some settings caches routes and config

Fields stored in .env trigger dotartisan:optimize, which runs config:cache and route:cache. On a development copy this makes new routes and config changes invisible until php artisan optimize:clear.

A settings label equal to a language file name crashes the screen

Labels go through trans(). A label such as 'Pages' or 'Settings' matches a language file and returns an array, and the screen fails with a type error. Use namespaced keys or full sentences.

Saving theme options replaces the whole set

ThemeOptionStore::put() stores exactly the array you pass for that theme. Merge with persistedForTheme() when you change one value from code.

The CDN settings screen is switched off

The underlying package does not support the current Laravel version, so the CDN section is disabled in RegisterSettings; config/cdn.php and the push route remain.

Themes and assets

A new build does not reach the public site by itself

The public site serves a compiled copy of Echo's stylesheet with the site's colours. After npm run build, use Theme options → Advanced → Regenerate Theme CSS.

The template library is tied to the theme name

Library rows are tagged with the theme they were published for. A theme with a different name sees none of Echo's layouts until templates are published for it. See Creating a theme.

Async widgets render outside the admin theme

Async dashboard widgets are loaded by a separate request that does not pass through AdminTheme. The core admin widgets call Theme::set('admin') in their constructor; do the same in yours if they render theme views.

Plugins and modules

The Plugin Manager does not run Composer, migrations or builds

After installing a plugin from a ZIP: composer dump-autoload, activate, php artisan migrate, run its seeder, and publish its assets. See Package, install and update.

Module translation models need an explicit class

config/translatable.php looks for translation models in App\Models\Translations. Module models must set public string $translationModel = ...::class;.

Module routes are not localized

Core public routes carry a locale prefix for non-default languages; Booking, TruCommerce and TruPayments routes do not. The language still follows the visitor's session.

Plugin tests are not picked up automatically

tests/Pest.php lists only the bundled modules' test folders. Add uses(TestCase::class, RefreshDatabase::class); to your test files. See Testing.

Payment methods follow the visitor's IP before the billing address

PaymentManager::availableMethods() uses the GeoIP location when it resolves a country and only falls back to the billing address you pass.

Background work

Queues are synchronous and broadcasting is unused

QUEUE_CONNECTION defaults to sync; BROADCAST_DRIVER is log with no client library installed. dotartisan:tickets:autoclose exists but is not scheduled.

Leftovers you can ignore