F
Filamentβ€’3mo ago
kakallatt

Multi tenancy with multiple database support

When reading the documentation, it appears that Filament only supports multi-tenancy with a single database by using team_id. Is implementing multiple databases in the plan, or is it easily achievable with a workaround, or do I need to use a third-party package like stancl/tenancy or spatie/laravel-multitenancy?
28 Replies
Dennis Koch
Dennis Kochβ€’3mo ago
It’s not planned. Best to use a 3rd party plugin.
EleventhTower
EleventhTowerβ€’2mo ago
Hmm. I need this functionality as well. I do not see any plugins that support multi-database tenancy. From searching the Discord chats here for Spatie Laravel-multitenancy, I see many problems, but none have a clear resolution. I read an old (2022) GitHub post, but the methods they listed to get it to work don't seem to map to V3 since the configuration file structure is different. There may be more issues than that, but I decided to search more since the post is a couple of years old. Has anyone gotten this to work?
Dennis Koch
Dennis Kochβ€’2mo ago
Yes, I have multi-database tenancy using stancl/tenancy v4-alpha, but I guess v3 should work, too.
EleventhTower
EleventhTowerβ€’2mo ago
@Dennis Koch that's great news. Do you remember the steps you took to get it working? I see the liverwire integration section in the docs, just wondering if there were any gotchas
Dennis Koch
Dennis Kochβ€’2mo ago
I think I was mostly following the docs. I read it 2 or 3 times because I missed some stuff. Only gotcha was manually symlinking tenants public folders.
EleventhTower
EleventhTowerβ€’2mo ago
Thanks for the heads up. I'll play around with this a bit later. I hadn't even looked at other laravel based packages besides Spatie
Dennis Koch
Dennis Kochβ€’2mo ago
Most of the pain I went through was because this wasn't a fresh app, but we added tenancy later on πŸ˜…
camya
camyaβ€’3w ago
Hi Dennis, great that you've got it working. I am suffering with stancl/tenancy and Filament at the moment. (I use Filament Form and Table separately). In stancl/tenancy the config "asset_helper_tenancy" is set to true. - Filament styles/scripts - Loaded from the wrong folder:
"@FilamentStyles and @FilamentScripts load files from the /tenancy/ folder, which does not work. (e.g. "/tenancy/assets/js/filament/support/async-alpine.js?v=3.2.80.0") - If I set asset_helper_tenancy = false it works. - FileUpload: File uploads are not saved to the subfolder of the tenancy folder (e.g. /storage/tenantsport/), so preview does not work. The tenancy folder (public disk) is set by stancl/tenancy to /Development/valet/laravel-sites/filament-demo/storage/tenantsport/app/public/, but files are saved to /Development/valet/laravel-sites/filament-demo/storage/app/public/ by the Filament FileUpload. It looks like Filament is not using the filesystems.disks.public.root configuration value when saving the file. I hope you can give me some ideas on how to make it work. Are there changes in v4-alpha, which makes it more easy to use stancl/tenancy and Filament together?
Dennis Koch
Dennis Kochβ€’3w ago
Are there changes in v4-alpha, which makes it more easy to use stancl/tenancy and Filament together?
They have public folder linking now, but it didn't work in ma case so I linked them manually πŸ˜…
Filament styles/scripts - Loaded from the wrong folder:
I think you need to disable the asset_helper_tenancy.
File uploads are not saved to the subfolder of the tenancy folder (e.g. /storage/tenantsport/), so preview does not work.
Did you set the Tenant middleware for the Livewire update route?
HerrChris
HerrChrisβ€’3w ago
I am just curious the reasons for multiple databases with multi tenancy? Is it a requirement to just have data separate or are there other benefits?
camya
camyaβ€’3w ago
@Dennis Koch Great tips. I used the two links below to set up "tenancy for laravel". I still have a problem with the preview url in my Filament Form. The uploaded files are correctly saved in the folder /Development/valet/laravel-sites/filament-demo/storage/tenantsport/post/01HY2S9NP685FD3TFBCXRAF7RF.png. Great. The problem is that the preview in Filament is still loading from https://sport.my-site.test/storage/post/01HY2S9NP685FD3TFBCXRAF7RF.png when it should be https://sport.my-site.test/storage/tenantsport/post/01HY2S9NP685FD3TFBCXRAF7RF.png. Do you have any idea what I am missing here? Tenancy for Laravel - Livewire: https://tenancyforlaravel.com/docs/v3/integrations/livewire/#livewire Tenancy for Laravel - Universal Routes: https://tenancyforlaravel.com/docs/v3/features/universal-routes
Dennis Koch
Dennis Kochβ€’3w ago
The problem is that the preview in Filament is still loading from
Did you configure your public disk in the tenancy.public config? Can you share that config? In v4-alpha it's called tenancy.filesystem.url_override. Maybe that was introduced with v4
camya
camyaβ€’3w ago
These are my settings. Still using the v3.8.3 of stancl/tenancy. 'filesystem' => [ 'suffix_base' => 'tenant', 'disks' => [ 'local', 'public', ], 'root_override' => [ 'local' => '%storage_path%/app/', 'public' => '%storage_path%/app/public/', ], 'suffix_storage_path' => true, 'asset_helper_tenancy' => false, ],
Dennis Koch
Dennis Kochβ€’3w ago
I guess there wasn't an option for that in v3 then.
camya
camyaβ€’3w ago
Hm, I have to wait for the v4 release than. Looks like symlinking the storage/tenant folders is also a challenge in v3. artisan storage:link does not work out of the box. Also this needs the tenancy.filesystem.url_override parameter too in order to set the correct url, i guess.
Dennis Koch
Dennis Kochβ€’3w ago
artisan storage:link only does the central public folder. You can just manually symlink the others. That shouldn't be an issue.
camya
camyaβ€’3w ago
I made it work kind of. Works for a first test. Ideas to optimize it are welcome. Tenant assets are now public available on https://sport.my-site.test/tenant/post/123.png I create the folders & public symlinks in CreateFrameworkDirectoriesForTenant
# config/tenancy.php
'filesystem' => [
'suffix_base' => 'tenant/',
]
# config/tenancy.php
'filesystem' => [
'suffix_base' => 'tenant/',
]
<?php

namespace App\Jobs;

class CreateFrameworkDirectoriesForTenant
{
protected $tenant;

public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}

public function handle()
{
$this->tenant->run(function ($tenant) {
$storage_path = storage_path();

$suffixBase = config('tenancy.filesystem.suffix_base');

if (!is_dir(public_path($suffixBase))) {
@mkdir(public_path($suffixBase), 0777, true);
}

if (!is_dir($storage_path)) {
@mkdir("{$storage_path}/app/public", 0777, true);
@mkdir("{$storage_path}/framework/cache", 0777, true);

symlink("{$storage_path}/app/public", public_path("{$suffixBase}{$tenant->id}"));
}
});
}
}
<?php

namespace App\Jobs;

class CreateFrameworkDirectoriesForTenant
{
protected $tenant;

public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}

public function handle()
{
$this->tenant->run(function ($tenant) {
$storage_path = storage_path();

$suffixBase = config('tenancy.filesystem.suffix_base');

if (!is_dir(public_path($suffixBase))) {
@mkdir(public_path($suffixBase), 0777, true);
}

if (!is_dir($storage_path)) {
@mkdir("{$storage_path}/app/public", 0777, true);
@mkdir("{$storage_path}/framework/cache", 0777, true);

symlink("{$storage_path}/app/public", public_path("{$suffixBase}{$tenant->id}"));
}
});
}
}
Added to...
Events\TenantCreated::class => [
JobPipeline::make([
CreateFrameworkDirectoriesForTenant::class,
])
],
Events\TenantCreated::class => [
JobPipeline::make([
CreateFrameworkDirectoriesForTenant::class,
])
],
In the custom route FileUrlMiddleware, i update the filesystems.disks.public.url config value.
use App\Http\Middleware\FileUrlMiddleware;

->withMiddleware(function (Middleware $middleware) {
$middleware->group('universal', [
FileUrlMiddleware::class
]);
})
use App\Http\Middleware\FileUrlMiddleware;

->withMiddleware(function (Middleware $middleware) {
$middleware->group('universal', [
FileUrlMiddleware::class
]);
})
<?php
namespace App\Http\Middleware;

class FileUrlMiddleware
{
public function handle(Request $request, Closure $next): Response
{
config()->set(
'filesystems.disks.public.url',
url('/' . config('tenancy.filesystem.suffix_base') . tenant('id'))
);

return $next($request);
}
}
<?php
namespace App\Http\Middleware;

class FileUrlMiddleware
{
public function handle(Request $request, Closure $next): Response
{
config()->set(
'filesystems.disks.public.url',
url('/' . config('tenancy.filesystem.suffix_base') . tenant('id'))
);

return $next($request);
}
}
CGM
CGMβ€’2w ago
Thank you @camya! I've been working on this literally all day. Your middleware idea is solid solution and was exactly what I was missing. Reading through https://discord.com/channels/883083792112300104/1180409835309764628 helped too for anyone trying to get Filament/Livewire/TenancyForLaravel working, but your solution is what solved it for me. I use a ton of different disks to keep things organized, so instead of having a single public disk I use several that I've prefixed with 'tenant' I need to refactor it, but all I have to do is just create a disk prefixed with 'tenant' and everything works with Tenancy, Filament v3 and Livewire. Of course follow the rest of the install/setup guide for tenancy regarding Livewire as well. config/filesystem.php example
'disks' => [
// --
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
'tenant_logos' => [
'driver' => 'local',
'root' => storage_path('app/public/logos'),
'url' => '/logos', // will be modified by FileUrlMiddleware::class
'visibility' => 'public',
'throw' => false,
],
'tenant_photos' => [
'driver' => 'local',
'root' => storage_path('app/public/photos'),
'url' => '/photos', // will be modified by FileUrlMiddleware::class
'visibility' => 'public',
'throw' => false,
],
],
'disks' => [
// --
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
'tenant_logos' => [
'driver' => 'local',
'root' => storage_path('app/public/logos'),
'url' => '/logos', // will be modified by FileUrlMiddleware::class
'visibility' => 'public',
'throw' => false,
],
'tenant_photos' => [
'driver' => 'local',
'root' => storage_path('app/public/photos'),
'url' => '/photos', // will be modified by FileUrlMiddleware::class
'visibility' => 'public',
'throw' => false,
],
],
then my middleware grabs each disk and applies the proper url: FileUrlMiddleware.php
class FileUrlMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Get tenant-specific disks
$tenantDisks = collect(config('filesystems.disks'))
->filter(fn($diskConfig, $key) => str_starts_with($key, 'tenant_'));

// Add in any other disks that should be updated that don't start with 'tenant_'
$disks = array_merge($tenantDisks->keys()->toArray(), ['public']);

// Update URL configuration for each disk
foreach ($disks as $disk) {
$url = url('/' . config('tenancy.filesystem.suffix_base') . tenant('id') . config("filesystems.disks.$disk.url"));
config()->set("filesystems.disks.$disk.url", $url);
}

return $next($request);
}
}
class FileUrlMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Get tenant-specific disks
$tenantDisks = collect(config('filesystems.disks'))
->filter(fn($diskConfig, $key) => str_starts_with($key, 'tenant_'));

// Add in any other disks that should be updated that don't start with 'tenant_'
$disks = array_merge($tenantDisks->keys()->toArray(), ['public']);

// Update URL configuration for each disk
foreach ($disks as $disk) {
$url = url('/' . config('tenancy.filesystem.suffix_base') . tenant('id') . config("filesystems.disks.$disk.url"));
config()->set("filesystems.disks.$disk.url", $url);
}

return $next($request);
}
}
And finally I added this to my TenancyServiceProvider to handle adding the tenant disks to the tenancy.php config (tenancy.filesystem.root_override and tenancy.filesystem.disks) automatically based on their names. I call this method in boot() fairly after setting middleware priority.
protected function setupTenantDisks()
{
// Get all disks from the configuration and filter for tenant-specific disks
$tenantDisks = collect(config('filesystems.disks'))
->filter(fn($diskConfig, $key) => str_starts_with($key, 'tenant_'))
->map(fn($diskConfig) => Str::replaceFirst('base_path() . '/storage' '%storage_path%', $diskConfig['root'] ?? '') . '/');

// Update the tenancy configuration with the tenant disk paths
config()->set(
'tenancy.filesystem.root_override',
array_merge(config('tenancy.filesystem.root_override', []), $tenantDisks->toArray())
);

// Update the tenancy configuration with the tenant disk keys
config()->set(
'tenancy.filesystem.disks',
array_merge(config('tenancy.filesystem.disks', []), $tenantDisks->keys()->toArray())
);
}
protected function setupTenantDisks()
{
// Get all disks from the configuration and filter for tenant-specific disks
$tenantDisks = collect(config('filesystems.disks'))
->filter(fn($diskConfig, $key) => str_starts_with($key, 'tenant_'))
->map(fn($diskConfig) => Str::replaceFirst('base_path() . '/storage' '%storage_path%', $diskConfig['root'] ?? '') . '/');

// Update the tenancy configuration with the tenant disk paths
config()->set(
'tenancy.filesystem.root_override',
array_merge(config('tenancy.filesystem.root_override', []), $tenantDisks->toArray())
);

// Update the tenancy configuration with the tenant disk keys
config()->set(
'tenancy.filesystem.disks',
array_merge(config('tenancy.filesystem.disks', []), $tenantDisks->keys()->toArray())
);
}
Edit: Fixed a hard-coded string that was causing problems. Should be more reliable.
Gaurav
Gauravβ€’6d ago
I might have missed something. What were the other changes you had done apart from this to have stancl/tenancy work with filament? Where exactly did you place this portion?
use App\Http\Middleware\FileUrlMiddleware;

->withMiddleware(function (Middleware $middleware) {
$middleware->group('universal', [
FileUrlMiddleware::class
]);
})
use App\Http\Middleware\FileUrlMiddleware;

->withMiddleware(function (Middleware $middleware) {
$middleware->group('universal', [
FileUrlMiddleware::class
]);
})
CGM
CGMβ€’6d ago
I used the FileUrlMiddleware in a few places. One was in my tenant.php routes.
Route::middleware([
PreventAccessFromCentralDomains::class,
'web',
TenancyServiceProvider::TENANCY_IDENTIFICATION,
ScopeSessions::class,
\App\Http\Middleware\FileUrlMiddleware::class,
])->group(function () {
// Routes here
}
Route::middleware([
PreventAccessFromCentralDomains::class,
'web',
TenancyServiceProvider::TENANCY_IDENTIFICATION,
ScopeSessions::class,
\App\Http\Middleware\FileUrlMiddleware::class,
])->group(function () {
// Routes here
}
Another was in my TenantPortalPanelProvider.php
->middleware([
'universal',
TenancyServiceProvider::TENANCY_IDENTIFICATION,
PreventAccessFromCentralDomains::class,
\App\Http\Middleware\FileUrlMiddleware::class,
])
->middleware([
'universal',
TenancyServiceProvider::TENANCY_IDENTIFICATION,
PreventAccessFromCentralDomains::class,
\App\Http\Middleware\FileUrlMiddleware::class,
])
And finally this in my TenancyServiceProvider.php
private function prepareLivewireForTenancy(): void
{
FilePreviewController::$middleware = ['web', 'universal', static::TENANCY_IDENTIFICATION];

Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle)
->middleware(
'web',
'universal',
static::TENANCY_IDENTIFICATION,
\App\Http\Middleware\FileUrlMiddleware::class
);
});
}
private function prepareLivewireForTenancy(): void
{
FilePreviewController::$middleware = ['web', 'universal', static::TENANCY_IDENTIFICATION];

Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle)
->middleware(
'web',
'universal',
static::TENANCY_IDENTIFICATION,
\App\Http\Middleware\FileUrlMiddleware::class
);
});
}
Pretty much anywhere that needs to get a proper route to the tenants version of the file URLs. I call this (method above) in my TenancyServiceProvider.php boot() method:
$this->prepareLivewireForTenancy();
$this->prepareLivewireForTenancy();
Gaurav
Gauravβ€’6d ago
For me things were kind of working alright with just setting tenancy.asset_helper_tenancy = false as there were no tenant specific assets. Things fell apart when I had to add Import functionality.
CGM
CGMβ€’6d ago
What kind of import functionality are you working with? CSV, or just uploads in general?
Gaurav
Gauravβ€’5d ago
CSV uploads using importer action
zenepay
zenepayβ€’5d ago
This's gonna be nice one, with complicated work.
Gaurav
Gauravβ€’5d ago
This is weird. After making all these changes, I can see the file upload issues getting solved, as in the fopen error went away. But for some reason the import and export jobs are failing now, as those are getting posted to central database. Although the corresponding actions (import/export) are posted to tenant database. The job ultimately fails after exceeding the number of attempts supported by the corresponding database field. File Exports were working last evening, but I hadn't committed the changes, as I wanted to do it right (via tenant folder). And just like that it started working again πŸ₯Ή
Gaurav
Gauravβ€’5d ago
Seems I spoke too early. So now the import and exports jobs are working, but the export URL like <subdomain>/filament/exports/1/download?format=csv is giving me 404. And the path on disk looks a bit different. Notice filament_exports in the folder path vis-a-vis filament/exports in the URL
No description
CGM
CGMβ€’5d ago
Depending on the changes you made above, you might need to modify tenancy.php config. Double check your tenancy.filesystem.suffix_base, tenancy.filesystem.disks and finally tenancy.filesystem.root_override. I try to do this in my setupTenantDisks() method above, but you can do it manually just as easily if you dont have too many disks. That's the first place I would check if tenant files are ending up in the base public directory.
Gaurav
Gauravβ€’2d ago
Actually the files are landing correctly in the tenant specific folder, as can be seen in the screenshot above. I think somehow the /filament/exports route is looking at the wrong place, probably the root public folder instead of tenant specific public folder Looks like the filament.exports route itself is throwing 404, as in action corresponding to the route not found from inside tenant application I am unable to figure out these 404. Has anyone else faced such an issue?