eugenefvdm / multi-tenancy-pwa
A Laravel package for Filament multi-tenancy & PWA
Installs: 826
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Language:Blade
pkg:composer/eugenefvdm/multi-tenancy-pwa
Requires
- laravel-notification-channels/webpush: ^10.2
- laravel/framework: ^12.0
- laravel/socialite: ^5.21
- spatie/eloquent-sortable: ^4.5
- spatie/laravel-ray: ^1.40
Requires (Dev)
- orchestra/testbench: ^10.3
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
README
A very opinionated setup script for Laravel and Filament PHP to quickly bootstrap a modern multi-tenant aware back office application.
It has PWA features for app installation and web push notifications.
It includes Google Socialite login (compliments of Povilas: https://laraveldaily.com/post/filament-sign-in-with-google-using-laravel-socialite)
General Features
- Works with Filament 4 for rapid application development
- Includes a
HasTenantRelationshiptrait that may be used to make models tenant aware - There is ocial login using Google making signing up of new users a breeze
PWA (Progressive App Features)
- An App installation button
- Web Push notifications
- PWA Diagnostics Screen
Prerequisites
- FilamentPHP version 4.x must be installed
Packages used
The following are presumed to be used and already included in composer:
- Laravel Socialite
- Spatie Eloquent Sortable
- Web push from Laravel Notification Channels
Installation
If Filament 4.x is still in beta:
composer config minimum-stability beta composer require filamentphp/filament:4.x
composer require eugenefvdm/multi-tenancy-pwa
If you're going to be sorting Filament resources any time soon
php artisan vendor:publish --tag=eloquent-sortable-config
Setup
Spatie orderable column name
In config/eloquent-sortable.php, change order_column_name from order_colulmn to sort.
If you're using this in your code you'll need both Sortable and SortableTrait.
Database migrations for Tenancy
Every tenant aware table in your application should have this line:
$table->foreignId('tenant_id')->constrained('tenants');
To keep things tidy I like putting them after the main ID column.
Tenant Model
You'll need the base tenant model:
php artisan make:model Tenant -f
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Tenant extends Model { /** @use HasFactory<\Database\Factories\TenantFactory> */ use HasFactory; protected $fillable = [ 'name', ]; public function users(): BelongsToMany { return $this->belongsToMany(User::class); } }
Model Trait
Add the following trait to your tenant aware models (but not to Tenant.php):
use Eugenefvdm\MultiTenancyPWA\Traits\HasTenantRelationship;
Filament Panel Service Provider
ApplyTenantScopes Middleware
Let's say you have todo and category tables and you want them to automatically get tenant_id. Use this middleware:
<?php namespace App\Http\Middleware; use App\Models\Category; use App\Models\Todo; use Closure; use Filament\Facades\Filament; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class ApplyTenantScopes { /** * Handle an incoming request. * * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next): Response { Category::addGlobalScope( fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant()), ); Todo::addGlobalScope( fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant()), ); return $next($request); } }
The security of your application is your responsbility. Be sure to read this part of the manual: https://filamentphp.com/docs/4.x/users/tenancy#tenancy-security
To automatically assign your tenant id column to every record creation and list view, add Tenant
Next, continue the Filament installation:
php artisan filament:install --panels
Open AdminPanelProvider.php and add this to the end, below the authMiddleware section:
use App\Http\Middleware\ApplyTenantScopes; use App\Models\Tenant; use Eugenefvdm\MultiTenancyPWA\Filament\Pages\Tenancy\RegisterNewTenant; // Filament AdminPanelProvider extra sections ->colors([ // Already exists 'primary' => Color::Cyan ]); ->profile(\App\Filament\Pages\Auth\EditProfile::class); ->tenant(Tenant::class) ->registration() ->tenantRegistration(RegisterNewTenant::class) ->tenantProfile(EditMyTenantProfile::class) ->renderHook( 'panels::auth.login.form.after', fn () => view('multi-tenancy-pwa::auth.socialite.google') ->tenantMiddleware([ ApplyTenantScopes::class, ], isPersistent: true) ->databaseNotifications();
Web Push Notifications
Composer will already be updated with laravel-notification-channels/webpush.
Add the config file:
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="config"
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"
Example notification:
<?php namespace App\Notifications; use Filament\Facades\Filament; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; use NotificationChannels\WebPush\WebPushChannel; use NotificationChannels\WebPush\WebPushMessage; class WebpushNotification extends Notification { use Queueable; /** * Create a new notification instance. */ public function __construct(private string $title = 'Default Title', private string $body = 'Default Body Text') { // } /** * Get the notification's delivery channels. * * @return array<int, string> */ public function via(object $notifiable): array { return [WebPushChannel::class]; } public function toWebPush($notifiable, $notification): WebPushMessage { $tenantId = Filament::getTenant()->id ?? 1; return (new WebPushMessage) ->title($this->title) ->body($this->body) ->action('Call to action', 'do_something') // This appears as a button on the notification ->options(['TTL' => 10000]) ->vibrate([300, 100, 400]) ->data(['url' => config('app.url') . '/admin/' . $tenantId . '/notifications']); // ->icon('/approved-icon.png') // ->data(['id' => $notification->id]) // ->badge() // ->dir() // ->image() // ->lang() // ->renotify() // ->requireInteraction() // ->tag() } /** * Get the array representation of the notification. * * @return array<string, mixed> */ public function toArray(object $notifiable): array { return [ // ]; } }
Database notifications
Laravel 11 and higher:
php artisan make:notifications-table
User Model
use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasDefaultTenant; use Filament\Models\Contracts\HasTenants; use Filament\Panel; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; class User extends Authenticatable implements FilamentUser, HasDefaultTenant, HasTenants { use HasPushSubscriptions; // ... protected $fillable = [ 'google_id' ]; // ... public function canAccessPanel(Panel $panel): bool { return true; } public function tenants(): BelongsToMany { return $this->belongsToMany(Tenant::class); } public function getTenants(Panel $panel): Collection { return $this->tenants; } public function canAccessTenant(Model $tenant): bool { return $this->tenants()->whereKey($tenant)->exists(); } public function getDefaultTenant(): ?Model { return $this->latestTenant()->first(); } public function latestTenant(): BelongsTo { return $this->belongsTo(Tenant::class, 'latest_tenant_id'); } }
Now you can migrate the database
php artisan vendor:publish --tag="multi-tenancy-pwa-migrations"
Add Google oAuth credentials
First see: https://fwd.host/ Redirects to: https://herd.laravel.com/docs/macos/advanced-usage/social-auth
Then go to: https://console.cloud.google.com/apis/credentials?pli=1
At Google's URL above, carefully copy out your CLIENT_ID and CLIENT_SECRET top right.
GOOGLE_CLIENT_ID=****** GOOGLE_CLIENT_SECRET=****** # For testing use the line below, for production, leave it out. # GOOGLE_REDIRECT=https://fwd.host/http://your-herd-site.test/auth/google/callback
Note: The callback route from Google needs to be wrapped in web for auth()->login to work.
For web push
VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY=
See: https://laravel-news.com/fwd-host
Logging in
Sign up for a new account:
https://app.test/admin/register
Tenancy tips
Excluding resources from tenancy
As per the manual, you can exclude Filament resources from Tenancy by doing this:
use Filament\Resources\Resource; protected static bool $isScopedToTenant = false;
Web push errors
openssl_pkey_new(): Private key length must be at least 384 bits, configured to 0
You can simulate this error on the command line on a Mac
php -r 'print_r(openssl_pkey_new());' Warning: openssl_pkey_new(): Private key length must be at least 384 bits, configured to 0 in Command line code on line 1
php -i | grep 'Openssl default config' Openssl default config => /etc/ssl/openssl.cnf sudo vi /etc/ssl/openssl.cnf sudo vi /opt/homebrew/opt/openssl@3/etc/openssl.cnf [ req ] default_bits = 2048 default_md = sha256 default_keyfile = privkey.pem
But this works:
OPENSSL_CONF=/opt/homebrew/opt/openssl@3/etc/openssl.cnf \
php -r 'var_dump(openssl_pkey_new(["private_key_type"=>OPENSSL_KEYTYPE_RSA]));'
Socialite Errors
Incorrect Client Secret
Client error: POST https://www.googleapis.com/oauth2/v4/token resulted in a 400 Bad Request response: { "error": "invalid_request", "error_description": "Missing required parameter: code" }
The Client secret is incorrect. It's the bottom value on Google's site.
PWA Images
If you have an SVG, you can resize it here: See: https://pwagenerator.test/
Application Icon Name on Android
short_name in manifest.json