Skip to content

Module Development — Developer Guide

Canonical checklist for building a business module. Copy the Leads module structure; do not invent a second pattern.

Registration recipe

  1. Catalog (production) — Ship an idempotent data migration that calls App\Support\Catalog\DefaultModuleRegistrar (slug, pricing defaults, status=published, is_default_included / is_billable). Also keep CatalogSeeder in sync for local/CI fresh DBs only — never rely on db:seed in production. Optional: Central Modules API for non-default / commercial catalog edits.
  2. Permissions (production) — Add {slug} => [view, create, update, delete, …] in config/tenant-permissions.php and default grants in config/tenant-default-role-permissions.php. Ship a data migration that calls TenantPermissionSynchronizer::grantMissingDefaultRolePermissions([...]) so existing workspaces receive grants additively. Modules never auto-grant permissions; roles do. Login never syncs RBAC.
  3. Routes — Tenant API:
php
Route::middleware(['auth:tenant-api', 'tenant.user', 'verified', 'module:{slug}', 'can:{slug}.view'])->group(function () {
    // …
});
  1. Domain code — Flat under existing namespaces (no Modules/ package):
LayerLocation
Modelsapp/Models/ + BelongsToTenant
Migrationsdatabase/migrations/
Factoriesdatabase/factories/
Seedersdatabase/seeders/Tenant/ for local/demo only; production catalog/permission rows use data migrations
Controllersapp/Http/Controllers/Tenant/Api/V1/
Form requestsapp/Http/Requests/Tenant/Api/V1/{Module}/
Resourcesapp/Http/Resources/Tenant/Api/V1/{Module}/
Policiesapp/Policies/
Servicesapp/Services/Tenant/
Events / Listenersapp/Events/, app/Listeners/
Notificationsapp/Notifications/Tenant/
  1. Frontendsrc/pages/{slug}/, API service, types, PERMISSIONS / QUERY_KEYS, nav item with permission and module, route under TenantProtectedRoute.
  2. Tests — Pest feature suite + Playwright test:e2e:{slug}.
  3. Docs — User / developer / production guides, API, database, CHANGELOG.

Logging (both required)

LayerMechanismPurpose
AuditPlatformAuditServiceactivity('platform')Actor, workspace, IP, UA, action for create/update/delete/assign/status changes
ActivitySpatie LogsActivity on primary modelAttribute-level change history
TimelineDomain *_activities table (when UX needs it)User-facing history (notes, stage moves, assignments)

Events

Dispatch domain events from the service layer. Listeners handle audit side-effects and notifications. Do not build per-module notification stacks outside Laravel notifications.

Dashboard widgets

When a module contributes dashboard cards:

  1. Extend App\Services\Tenant\DashboardWidgetService (same registry pattern as Leads/Tasks).
  2. Gate each widget on EntitlementService::hasModule + the user’s Spatie permission.
  3. Apply assignee scoping with ScopesToAssignee when the module uses {slug}.assign.
  4. Return { id, module, permission, scope, data } objects only — the SPA renders by id.
  5. Do not invent a parallel dashboard API; extend DashboardWidgetService (Leads/Tasks/Calendar pattern).

See tenant-v1-dashboard.md.

In-app notifications

  • Use Laravel notification channels required by the contract for that event (database plus broadcast for realtime in-app delivery; add mail only when product behavior requires it).
  • Implement ShouldQueue and use App\Notifications\Concerns\QueuesOnEmails so jobs land on the dedicated emails queue (php artisan queue:work --queue=emails).
  • Persist via the standard notifications table; expose tenant APIs under /notifications* (tenant-v1-notifications.md).
  • Register realtime in-app types in the SPA Notification Registry; Echo updates Query caches and polling remains a recovery path.
  • Schedule due/overdue fan-out through crm:send-due-notifications rather than ad-hoc cron per module.

Settings

If the module needs settings, register keys in SystemSettingDefinitions / TenantSettingDefinitions and resolve Central → Tenant → system. Do not invent a parallel settings store.

Billing

Paid modules: catalog is_billable, marketplace install, ModuleSubscriptionService, consolidated billing. Never implement independent payment flows.

Frontend checklist

  • Pages, forms, tables, filters, dialogs/drawers
  • Shared design system (PageHeader, DataTable, PermissionGate, empty/error/loading states)
  • Nav + breadcrumbs respect installed modules and user permissions
  • Auth payload includes active module slugs for SPA gating

Testing checklist

Pest: unit where useful; feature CRUD; authorization; validation; tenant isolation; module middleware denial.

Playwright: dedicated suite; script test:e2e:{slug}; independently runnable.

Manual QA: Cursor browser — CRUD, search, filters, pagination, validation, authz, responsive layout, console, network.

Anti-patterns

  • Laravel Modules / nwidart / plugin discovery
  • Repositories layer
  • Skipping module: or can: middleware
  • Static nav that ignores entitlements
  • Custom audit/notification systems outside platform services
  • Production db:seed / CatalogSeeder to register default modules
  • Login-time or dashboard-time permission “repair”
  • syncPermissions() on existing customized roles during deploy

Official documentation for the SaleOS SaaS Platform.