Skip to content
← Back to projects

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

  1. 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(),
        ]);
}
  1. 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,
],
  1. Add @source to your Filament theme (e.g. resources/css/filament/{panel}/theme.css):
@source '../../../../vendor/alessandro-nuunes/filament-tenancy-onboarding/resources/views/**/*';
  1. Run npm run build

#Integration with existing tenants

If your project already has tenants and tenant_users tables with a custom schema:

  1. Migrations — The publish may overwrite your create_tenants_table and create_tenant_users_table migrations. After install, restore them via Git if needed, or publish only tenant_invites and registration_tokens:

    php artisan vendor:publish --tag="filament-tenancy-onboarding-migrations"
    

    Then exclude or restore tenants/tenant_users migrations manually according to your schema.

  2. invitation_token column — The tenants table needs this column (string, nullable, unique) for generic invites and AcceptInvite. Create a migration:

    $table->string('invitation_token')->nullable()->unique()->after('slug');
    
  3. Trait on Tenant model — Use the trait to get invitation_token in fillable and the users() relationship:

    use AlessandroNuunes\FilamentTenancyOnboarding\Concerns\HasTenancyOnboarding;
    
    class Tenant extends Model
    {
        use HasTenancyOnboarding;
        // ...
    }
    
  4. Pivot columns — If tenant_users has extra columns (e.g. permissions, is_active), configure in config/filament-tenancy-onboarding.php:

    'relationships' => [
        'tenant_user_pivot_columns' => ['role', 'permissions', 'is_active'],
        // ...
    ],
    
  5. Use your own Tenant model — If your tenants table has required columns (e.g. document, user_id), configure models.tenant with your model, not the plugin's. The plugin's default model has minimal $fillable; extra attributes from tenant_attributes are ignored during mass assignment:

    'models' => [
        'tenant' => App\Models\Tenant::class,
        'tenant_user' => App\Models\TenantUser::class,
        // ...
    ],
    
  6. tenant_attributes for required columns — If the table requires fields like document, user_id, etc., use tenant_attributes to 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 tenant must have these attributes in $fillable.

  7. Config cache — Closures do not work with config:cache. In development, use php 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 hooksgetTokenMetadata(), getExtraFormFillData(), afterTenantCreated(), shouldShowStepLabels() — override to customize without rewriting methods
  • Lifecycle eventsRegistrationTokenCreated and TenantCreatedFromRegistration for 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.

New version available.