Filament Tenancy Onboarding
A Filament plugin for FilamentTenancyOnboarding.
#Filament Tenancy Onboarding
Filament plugin for multi-tenant onboarding: step-by-step registration with email confirmation, workspace creation, and member management with invites.
#Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
- Filament 4.x or 5.x
#Installation
composer require alessandro-nuunes/filament-tenancy-onboarding
Publish config, migrations and translations:
php artisan filament-tenancy-onboarding:install
Or individually:
php artisan vendor:publish --tag="filament-tenancy-onboarding-config" php artisan vendor:publish --tag="filament-tenancy-onboarding-migrations" php artisan vendor:publish --tag="filament-tenancy-onboarding-translations"
Run migrations:
php artisan migrate
#Quick Start
- Register the plugin in your Filament panel:
use AlessandroNuunes\FilamentTenancyOnboarding\FilamentTenancyOnboardingPlugin; public function panel(Panel $panel): Panel { return $panel ->id('mei') ->path('mei') ->login() ->plugins([ FilamentTenancyOnboardingPlugin::make(), ]); }
- Configure models in
config/filament-tenancy-onboarding.php:
'models' => [ 'user' => App\Models\User::class, 'tenant' => App\Models\Admin\Tenant::class, 'tenant_user' => App\Models\Admin\TenantUser::class, 'tenant_invite' => AlessandroNuunes\FilamentTenancyOnboarding\Models\TenantInvite::class, ],
- Add
@sourceto your Filament theme (e.g.resources/css/filament/{panel}/theme.css):
@source '../../../../vendor/alessandro-nuunes/filament-tenancy-onboarding/resources/views/**/*';
- Run
npm run build
#Integration with existing tenants
If your project already has tenants and tenant_users tables with a custom schema:
-
Migrations — The publish may overwrite your
create_tenants_tableandcreate_tenant_users_tablemigrations. After install, restore them via Git if needed, or publish onlytenant_invitesandregistration_tokens:php artisan vendor:publish --tag="filament-tenancy-onboarding-migrations"
Then exclude or restore tenants/tenant_users migrations manually according to your schema.
-
invitation_tokencolumn — Thetenantstable needs this column (string, nullable, unique) for generic invites and AcceptInvite. Create a migration:$table->string('invitation_token')->nullable()->unique()->after('slug');
-
Trait on Tenant model — Use the trait to get
invitation_tokenin fillable and theusers()relationship:use AlessandroNuunes\FilamentTenancyOnboarding\Concerns\HasTenancyOnboarding; class Tenant extends Model { use HasTenancyOnboarding; // ... }
-
Pivot columns — If
tenant_usershas extra columns (e.g.permissions,is_active), configure inconfig/filament-tenancy-onboarding.php:'relationships' => [ 'tenant_user_pivot_columns' => ['role', 'permissions', 'is_active'], // ... ],
-
Use your own Tenant model — If your
tenantstable has required columns (e.g.document,user_id), configuremodels.tenantwith your model, not the plugin's. The plugin's default model has minimal$fillable; extra attributes fromtenant_attributesare ignored during mass assignment:'models' => [ 'tenant' => App\Models\Tenant::class, 'tenant_user' => App\Models\TenantUser::class, // ... ],
-
tenant_attributesfor required columns — If the table requires fields likedocument,user_id, etc., usetenant_attributesto provide them on registration:'tenant_attributes' => function ($user, array $data): array { return [ 'document' => '00000000000000', 'user_id' => $user->id, 'phone' => $user->phone ?? null, ]; },
The model configured in
tenantmust have these attributes in$fillable. -
Config cache — Closures do not work with
config:cache. In development, usephp artisan config:clear. In production, register the closure in a Service Provider.
#Features
- Step-by-step registration — Name, email, phone → Email confirmation → Password + Workspace
- Token metadata — Pass extra data between registration steps via JSON metadata (e.g. CNPJ, referral code)
- Extra wizard steps — Insert custom steps between password and workspace via
getExtraStepsAfterPassword()with automatic heading tracking - Extensible hooks —
getTokenMetadata(),getExtraFormFillData(),afterTenantCreated(),shouldShowStepLabels()— override to customize without rewriting methods - Lifecycle events —
RegistrationTokenCreatedandTenantCreatedFromRegistrationfor listeners and side-effects - Phone mask — Configurable input mask for the phone field (e.g.
(99) 99999-9999) - Password strength rules — Configurable via
password_rules(min length, letters, numbers, mixed case, symbols, uncompromised) - Loading state — Smooth loading indicator on the confirmation page while the wizard initializes
- Expired/invalid links — Friendly error page when a signed link expires or is invalid
- Revealable passwords — Password fields include a toggle to show/hide the typed password
- Member management — Email invites, roles (owner/admin/member), generic invite link
- Tenant settings — Edit name, slug, ownership transfer
- Configurable permissions — Roles or closures (Shield, Spatie, etc.)
- Translations — pt_BR and en included
#Documentation
Full documentation in docs/PLUGIN-DOCUMENTATION.md:
- Registration flow and wizard
- Complete configuration
- Permission system
- Member management
- Tenant settings
- Page customization
- Upgrade guide
#Registration Configuration
#Phone mask
Configure the phone field input mask, strip characters, and placeholder:
'registration' => [ 'phone_mask' => '(99) 99999-9999', 'phone_strip_characters' => ['(', ')', ' ', '-'], 'phone_placeholder' => '(11) 99999-0000', ],
Set phone_mask to null to disable masking.
#Password rules
Configure password strength requirements:
'registration' => [ 'password_rules' => ['min:8', 'letters', 'numbers'], ],
Available rules: min:N, letters, mixedCase, numbers, symbols, uncompromised. When empty, falls back to Password::default().
#Loading screen
The confirmation page shows a loading spinner while the wizard initializes. Configure the minimum delay (in milliseconds) or disable it:
'registration' => [ 'loading_delay' => 1500, // default: 1500ms ],
Set to 0 to disable the loading screen entirely and show the wizard immediately:
'registration' => [ 'loading_delay' => 0, ],
#Registration Metadata & Hooks
The plugin supports passing extra data between registration steps (Step 1 → email → Step 2) via token metadata, and provides hooks and events for extending the registration flow without overriding entire methods.
#Token metadata
The registration_tokens table includes a metadata JSON column. Use it to transport data collected in Step 1 (e.g. CNPJ, referral code) to Step 2.
Override getTokenMetadata() in your custom RegisterStep1 page:
use AlessandroNuunes\FilamentTenancyOnboarding\Pages\RegisterStep1 as BaseRegisterStep1; class RegisterStep1 extends BaseRegisterStep1 { protected function getStep1FormSchema(): array { return [ ...parent::getStep1FormSchema(), TextInput::make('document') ->label('CNPJ') ->required() ->mask('99.999.999/9999-99') ->stripCharacters(['.', '/', '-']), ]; } protected function getTokenMetadata(array $data): array { return [ 'document' => $data['document'] ?? null, ]; } }
The metadata is automatically stored in the token and available in Step 2.
#Pre-filling the wizard (Step 2)
Override getExtraFormFillData() in your custom RegisterConfirm page to pre-fill wizard fields from token metadata:
use AlessandroNuunes\FilamentTenancyOnboarding\Pages\RegisterConfirm as BaseRegisterConfirm; class RegisterConfirm extends BaseRegisterConfirm { protected function getExtraFormFillData(): array { return $this->registrationToken?->metadata ?? []; } }
By default, getExtraFormFillData() already returns the token metadata. Override it only if you need custom logic.
#After tenant created hook
Override afterTenantCreated() to create related models after the tenant and pivot are created:
use Illuminate\Database\Eloquent\Model; class RegisterConfirm extends BaseRegisterConfirm { protected function afterTenantCreated(Model $tenant, object $user, array $data): void { $tenant->company()->create([ 'cnpj' => $data['document'] ?? null, 'company_name' => $data['company_name'] ?? null, ]); } }
#Available hooks
| Hook | Class | Description |
|---|---|---|
getTokenMetadata(array $data): array |
RegisterStep1 |
Extra data to store in the token. Default: [] |
getExtraFormFillData(): array |
RegisterConfirm |
Extra data to pre-fill in the wizard. Default: token metadata |
afterTenantCreated(Model $tenant, object $user, array $data): void |
RegisterConfirm |
Runs after tenant + pivot creation. Default: noop |
#Extra Wizard Steps
Add custom steps between the password and workspace steps without overriding getWizardSteps(). The plugin handles heading tracking and index calculation automatically.
#Adding a step
Override getExtraStepsAfterPassword() in your custom RegisterConfirm:
use AlessandroNuunes\FilamentTenancyOnboarding\Pages\RegisterConfirm as BaseRegisterConfirm; use Filament\Forms\Components\TextInput; use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Wizard\Step; class RegisterConfirm extends BaseRegisterConfirm { protected function getExtraStepsAfterPassword(): array { return [ Step::make('Company') ->schema([ Section::make('Company details') ->schema([ TextInput::make('company_name')->label('Company Name'), TextInput::make('trading_name')->label('Trading Name'), ]) ->columns(2), ]), ]; } }
The wizard becomes: Password → Company → Workspace → Welcome.
#Headings and subheadings for extra steps
Provide headings/subheadings in the same order as your extra steps:
protected function getExtraStepHeadings(): array { return ['Company Information']; } protected function getExtraStepSubheadings(): array { return ['Fill in your company details.']; }
#Hiding step labels
Remove the text labels from wizard steps (keeps only the step indicators):
protected function shouldShowStepLabels(): bool { return false; }
#Available hooks
| Hook | Class | Description |
|---|---|---|
getExtraStepsAfterPassword(): array |
RegisterConfirm |
Extra Step objects between password and workspace. Default: [] |
shouldShowStepLabels(): bool |
RegisterConfirm |
Show/hide step labels. Default: true |
getExtraStepHeadings(): array |
RegisterConfirm |
Page headings for extra steps (same order). Default: [] |
getExtraStepSubheadings(): array |
RegisterConfirm |
Page subheadings for extra steps (same order). Default: [] |
#Navigation & Clusters
#Where to display tenant settings (tenant_settings_location)
Choose where the tenant settings page appears:
| Value | Description |
|---|---|
cluster (default) |
In the cluster defined by tenant_settings_page.cluster |
tenant_menu |
Link in the tenant dropdown menu, below "Add workspace" |
navigation |
As a standalone sidebar item (no cluster) |
['tenant_menu', 'cluster'] |
In both the tenant menu and the cluster |
'tenant_settings_location' => 'tenant_menu', // or 'cluster', 'navigation', or an array
Or via environment:
TENANT_SETTINGS_LOCATION=tenant_menu
#Customizing navigation labels
Navigation labels, group, icon and sort are configurable per page:
'navigation' => [ 'tenant_settings_page' => [ 'group' => 'Company', 'sort' => 1, 'icon' => 'heroicon-o-cog', 'label' => 'Settings', ], 'tenant_members_page' => [ 'group' => 'Company', 'sort' => 2, 'icon' => 'heroicon-o-users', 'label' => 'Team', ], ],
For translatable labels, keep label as null and override the plugin's translations in lang/vendor/filament-tenancy-onboarding/{locale}/default.php.
#Assigning pages to a Cluster
Set the cluster key to a Filament Cluster class:
'navigation' => [ 'tenant_settings_page' => [ 'cluster' => App\Filament\App\Clusters\Settings::class, ], 'tenant_members_page' => [ 'cluster' => App\Filament\App\Clusters\Settings::class, ], ],
#Events
The plugin dispatches events at key points in the registration flow. Listen to them in your EventServiceProvider or with Event::listen().
#RegistrationTokenCreated
Dispatched after a registration token is created and before the confirmation email is sent.
use AlessandroNuunes\FilamentTenancyOnboarding\Events\RegistrationTokenCreated; Event::listen(RegistrationTokenCreated::class, function (RegistrationTokenCreated $event) { // $event->token — RegistrationToken model // $event->user — User model // $event->formData — Step 1 form data });
#TenantCreatedFromRegistration
Dispatched after the tenant, pivot, and afterTenantCreated() hook run during the workspace step.
use AlessandroNuunes\FilamentTenancyOnboarding\Events\TenantCreatedFromRegistration; Event::listen(TenantCreatedFromRegistration::class, function (TenantCreatedFromRegistration $event) { // $event->tenant — Tenant model // $event->user — User model // $event->formData — Full wizard form data });
#Upgrading
#Adding metadata column (existing installations)
If you already have the registration_tokens table without the metadata column, create a migration:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { $table = config('filament-tenancy-onboarding.tables.registration_tokens', 'registration_tokens'); if (Schema::hasColumn($table, 'metadata')) { return; } Schema::table($table, function (Blueprint $table): void { $table->json('metadata')->nullable()->after('token'); }); } public function down(): void { $table = config('filament-tenancy-onboarding.tables.registration_tokens', 'registration_tokens'); Schema::table($table, function (Blueprint $table): void { $table->dropColumn('metadata'); }); } };
All new features are fully backward-compatible — no breaking changes for existing projects.
#Configuration examples
Examples for common scenarios in config/examples/:
- shield.php — Filament Shield integration
#License
MIT License — see LICENSE.md.