Module Development — Production Guide
Licensing
- Active license =
workspace_module_subscriptionsintrial/active(and not ended). EnsureModule(module:{slug}) returns 403/402-style denial when the workspace lacks the module.- Default-included modules (
is_default_included) install on workspace provision; platform admins may deactivate. - Entitlements cache:
workspace:{id}:entitlements(1 hour); invalidated on install/cancel/deactivate.
Workspace bootstrap (explicit provisioning)
On workspace create (TenantProvisioningService):
- Billing profile
- Default modules installed
- Authorization defaults via
TenantAuthorizationProvisioningService(roles + permission maps) - Module-specific seed data (e.g. default lead stages)
- Owner user created (
TenantAuthBootstrapService::createOwner) — no RBAC side effects on login afterward
Authentication, dashboard, and role/user listing never repair permissions.
Shipping a new default-included module
Use data migrations, not production seeders:
- Schema migration(s)
- Catalog data migration →
App\Support\Catalog\DefaultModuleRegistrar - Permission data migration →
App\Support\Permissions\TenantPermissionSynchronizer::grantMissingDefaultRolePermissions([...]) - Deploy with:
php artisan migrate --force
php artisan optimizeKeep CatalogSeeder updated for local/CI fresh databases only. Do not run db:seed / CatalogSeeder in production to register modules.
Never syncPermissions() against existing customized roles during deploy. Grants must be additive and idempotent.
What DefaultModuleRegistrar guarantees
firstOrCreateby module slug — never overwrites commercial flags, prices, renamed fields, orversionon existing rows- Optional
versionon create (defaults to1.0.0); bump existing catalog rows withbumpVersion($slug, $version)in a later data migration - Installs subscriptions only when a workspace has never had a row for that module (checks soft-deleted)
- Does not reactivate cancelled / suspended / soft-deleted subscriptions
Bumping modules.version on module updates
When a release meaningfully changes an existing module, bump catalog semver with an idempotent data migration:
| Change | Bump |
|---|---|
| Fix / polish | PATCH (1.0.1) |
| Additive feature / schema / API / UX | MINOR (1.1.0) |
| Large backward-compatible milestone | MAJOR (2.0.0) |
Modules ship without breaking changes. A bump updates the central catalog version and ships with the platform deploy — every entitled workspace gets the new behavior. It does not install or re-enable the module for workspaces that never subscribed (or cancelled / suspended).
app(\App\Support\Catalog\DefaultModuleRegistrar::class)
->bumpVersion('meetings', '1.1.0');Do not rely on CatalogSeeder / re-running ensureModule — those paths never overwrite catalog version. See Module Development — Developer Guide.
What TenantPermissionSynchronizer guarantees
- Creates missing permission vocabulary from
config/tenant-permissions.php - Grants only the listed new permission names to default roles that should have them
- Leaves customized role edits intact
Monitoring
- Platform audit log (
activitylog nameplatform) for install, assign, destructive actions - Spatie activity on domain models for attribute changes
- Nightwatch / Telescope for exceptions on module routes
- Stripe / gateway logs remain under Billing — modules must not bypass them
Deploy checklist
- Run migrations (
php artisan migrate --force) — include data migrations for catalog rows,bumpVersionwhen the module changed, and additive permission grants - Do not rely on
CatalogSeeder/db:seedin production for new default modules or version bumps - Confirm
tenant-permissions+tenant-default-role-permissionsconfig are deployed with the release (migrations read them) - Entitlement cache is cleared per workspace by the module registrar when a subscription is newly installed
- If the module contributes dashboard widgets or notifications, confirm scheduler (
crm:send-due-notifications) and SPA polling/widget ids - If the module uses live conversation rooms (Team Chat), confirm Reverb is up and broadcast auth allows
tenant.{tid}.conversation.{id}in addition totenant.{tid}.user.{uid}; always scheduleteam-chat:purge-expired(no-op whenteam-chat.retention_daysis0) - Always schedule
trash:purge-expired(no-op whentrash.retention_daysis0) so SoftDeletes Trash stays within the workspace retention window - If the module has dates, schedules, digests, or office hours, confirm Workspace timezone convention (non-UTC workspace smoke)
- Smoke: login → module nav visible → list API 200 with
module:+can:→ Marketplace shows expected version / Available|Installed|Billable badges
Rollback
- Soft-delete / deactivate module subscription to revoke licensing without dropping data
- Keep domain tables; do not drop migrations in production without a data plan
- Permission grant migrations are intentionally irreversible (do not revoke production role grants in
down())