The project's own guidelines, collected in one place. Follow them and your plugin will look like the bundled modules — which is the point.
resources/themes/{admin,echo}/… or in a module's resources/views, with small vanilla JavaScript where needed. The Vue-based Translations screen comes from a package and is not a pattern to follow.Modules/{Name}. Core routes/*.php never load module routes; each module wires its own through its RouteServiceProvider. Do not add e-commerce, booking or payment code to app/.AdminBooting event and the App\Facades\{AppPages,Dashboard,Widgets,…} registries). No direct Modules\* references from app/Models.->can('permission string') on the route (Spatie permissions), not policy classes — except per-instance ownership checks like Team and Ticket.slugs table) through HasSlug. New sluggable models implement ResolvableSlug and a ResolvedSlugRenderer, not a bespoke route.App\Services\Blog\Publication). Configurable blog layouts were tried and removed on purpose; do not add a layout registry, picker or blog settings store.Post.status, Ticket.state). Booking and TruPayments do declare a few enums of their own (for example AppointmentStatus, PaymentStatus); match the convention of the area you work in.sync and broadcasting is dormant (BROADCAST_DRIVER=log). Check the deployment before you depend on either.composer.json or package.json; if it needs a PHP library, declare it in the module's own composer.json and document the Composer step. Do not add new top-level folders.The code is formatted with Laravel Pint (preset: laravel, one space around string concatenation). Run vendor/bin/pint --dirty (or vendor/bin/pint Modules/YourPlugin) before finishing any PHP change.
declare(strict_types=1);.__construct().function isAccessible(User $user, ?string $path = null): bool).array{id: string, name?: string}). Inline comments only for genuinely complex logic.isRegisteredForDiscounts, not discount()). Enum cases (when you do use enums) are TitleCase.php artisan make:* (or module:make-* inside a module) with --no-interaction so the stubs and namespaces are right.env() outside config/; read configuration with config().| Thing | Convention | Example |
|---|---|---|
| Module name / alias | StudlyCase name; lowercase kebab alias (2–64 chars) | TruCommerce / trucommerce |
| Provider | {Name}ServiceProvider | BookingServiceProvider |
| Admin route names | admin.{module}.{resource}.{action} | admin.booking.services.store |
| Front route names | {module}.{page} | booking.services.index |
| Permissions | Lowercase English phrases: manage x, create x, edit x, delete x, view x | manage booking services |
| Permission groups | kebab-case, prefixed with the module | booking-refunds |
| Setting keys | snake_case, prefixed with the module alias | trucommerce_page_shop |
| View namespaces | Module alias | booking::admin.dashboard |
| Translation keys | namespace::file.key for modules; file.key for core | booking::booking.services |
| Shortcode ids | kebab-case, module-prefixed | booking-services |
| Shortcode tags (Blade components) | x-shortcode-{id} | <x-shortcode-booking-services> |
| Widget registry keys | kebab-case, module-prefixed | trucommerce-cart-mini |
| Dashboard item ids | dotted {module}.{item} | booking.counters |
| Facade container keys | lowercase one-word strings | menumanager, app-pages |
| Scheduled commands | {module}:{verb}-{noun} | booking:cleanup-payment-intents |
mb-5 = 2.5rem, mb-6 = 3.5rem) — read the compiled CSS, do not assume Bootstrap defaults.__() / @lang. Add keys to the language file of your module (lang/en/{file}.php).vite.config.jsinput list (see Build and assets).var(--bs-primary), color-mix), never hard-coded hex values.Blade::render(), so never let data you bake into a page contain unescaped {{ }} or @php.database/migrations); module paths are auto-discovered by nwidart. A module's tables are never dropped when the module is deactivated or deleted.firstOrCreate / updateOrInsert) and are also written as migrations when a permission is added after release (see the Booking refund permissions migration).php artisan test --compact path), and re-run after each change.Storage::fake('public').A few areas carry decisions the code cannot express. In the source repository each has a rule file in .ai/rules/; the essence is below. Follow it whenever you change or override files in these areas:
| Applies to | Rule file | Essence |
|---|---|---|
app/Services/PageBuilder/Bindings/** | bindings.md | Costly record fields are LazyValue so resolve() keeps a constant query count; extra baker markers (lists, counts, show-if, vars, attrs) |
resources/themes/echo/page-templates/** | page-templates.md | Theme colours only; layout CSS in sibling .css files (database), never in Echo Sass; motion layouts use the lm- prefix; dividers follow the site; data-bound markers |
resources/themes/echo/assets/sass/components/_page-sections.scss | components.md | The ui-* kit is compiled by npm run templates:kit, stored in template_assets, and output only for the classes a page uses |
Echo header components, menus, _header.scss, _theme-options.scss, ThemeCssCompiler | header.md | Menus take colours from --bs-header-* tokens; header presets, aliases, and the CSS "fit" mechanism; top-bar HTML is sanitised |
resources/themes/echo/assets/sass/** | sass.md | Stored page content makes Echo class names public API; check history before deleting a class |
database/seeders/Niches/** | niches.md | Niches supply data; palettes are derived from brand colours (NichePalette); presentation lives in NicheStyles |
app/Services/WebsiteTemplates/** | website-templates.md | Template packages are data only, never code; strict allow-lists and asset handling |
scripts/capture-* | scripts.md | Screenshot scripts use a scratch _previews database and must never run in parallel |