The smallest complete app showing database-per-tenant multi-tenancy in Filament: Laravel 13 + Filament 5 + stancl/tenancy v4, wired by the Packstub Tenancy plugin.
One login on the central domain, a workspace per tenant on its own subdomain, every tenant on its own database — no tenant_id columns, no global scopes.
Note
This is a reference, not a framework — deliberately small so every file is about one thing. The plugin itself is commercial and not in this repo; it installs from the Packstub registry with the token you get on purchase. All of this code is readable before buying.
You need PHP 8.4+, Composer, a Packstub access token (pkg_…, from your dashboard), and wildcard local DNS — Laravel Herd or Valet handle *.test automatically.
git clone https://github.com/packstub/filament-tenancy-demo.git && cd filament-tenancy-demo
composer config --auth http-basic.packstub.dev pkg_xxxxxxxxxxxxxxxx your-token-secret # writes git-ignored auth.json
composer setup # install, migrate, seed two tenants on their own SQLite DBs
herd link filament-tenancy-demo # → http://filament-tenancy-demo.test
composer run dev # server + queue worker + vite (provisioning needs the worker)Open http://filament-tenancy-demo.test — the landing page links everything below.
Tip
Not using Herd? Add 127.0.0.1 filament-tenancy-demo.test acme.filament-tenancy-demo.test globex.filament-tenancy-demo.test to /etc/hosts, set APP_URL=http://filament-tenancy-demo.test:8000, and php artisan serve. Every tenant you create later needs its own hosts line — which is why wildcard DNS is the recommended setup.
Two accounts, both with password packstub-tenancy-demo (while DEMO_LOGIN_PREFILL=true the first is pre-filled and the login page has a Sign in as viewer button):
| Login | Acme | Globex |
|---|---|---|
demo@example.com |
owner | owner |
viewer@example.com |
owner | member — Projects is read-only, no inviting |
Roles are per tenant: they live on the central tenant_user pivot (role column), and ProjectPolicy + the Members page read it through User::isOwnerOf($tenant).
| URL | What it is |
|---|---|
filament-tenancy-demo.test/admin |
Central: login, tenant switcher, onboarding wizard (/admin/new) |
acme.filament-tenancy-demo.test/admin |
Tenant Acme Inc. — its own SQLite database |
globex.filament-tenancy-demo.test/admin |
Tenant Globex Corp. — a different database |
…/admin/projects |
An ordinary Filament resource, isolated per tenant |
…/admin/profile |
Rename the workspace, change its slug and avatar |
…/admin/members |
Who's in this workspace; owners invite by email, change roles, remove members |
filament-tenancy-demo.test/central |
Operator panel (no tenancy): all tenants, retry provisioning, delete a tenant (drops its DB) |
- Sign in — you land in Acme's panel.
- Add a project, switch to Globex. It isn't there: each tenant reads and writes its own database.
- Create organization in the switcher. The queue creates a database, migrates and seeds it, and redirects you in.
Everything tenancy-specific is in these files; the rest is a stock laravel new + filament:install --panels.
| File | Role |
|---|---|
app/Providers/Filament/AdminPanelProvider.php |
->plugin(TenancyPlugin::make()) — the one line that wires tenant model, subdomain routing, switcher, onboarding, provisioning page. No stancl middleware. |
app/Providers/Filament/CentralPanelProvider.php, app/Filament/Central/ |
A second panel without the plugin, pinned to the central domain — the pattern for any operator/back-office panel. |
app/Models/User.php |
CentralConnection + HasPackstubTenants: users, sessions, and auth stay in the central database. |
config/packstub-tenancy.php |
Plugin config. Only change from the default: 'seeder' => TenantSeeder::class. |
config/tenancy.php |
stancl config as published by the installer — tenant model set, DatabaseSessionBootstrapper disabled. |
database/migrations/tenant/ |
Runs against each tenant database. Holds projects only — never users/sessions. |
database/seeders/TenantSeeder.php |
Tenant-safe seeder the provisioning pipeline runs inside every new tenant DB. |
database/seeders/DatabaseSeeder.php |
Demo data: the owner + two tenants, created through TenantOnboarder exactly like the wizard. |
app/Models/Project.php, app/Filament/Tenant/Resources/Projects/ |
A plain model and resource. Nothing tenant-aware in them — the connection is. |
app/Filament/Pages/Auth/Login.php |
Stock login page that pre-fills the demo account. Not tenancy-related — delete it in your app. |
tests/TenantTestCase.php, tests/Feature/TenantSmokeTest.php |
Tenant-aware test harness (sync queue, SQLite tenant DBs in a temp dir) and the end-to-end smoke test. |
How provisioning works, what lives in which database, and why there's no stancl middleware: How it works in the plugin docs.
php artisan testTenantSmokeTest seeds the demo, asserts both tenants are ready, that each tenant database holds only its own projects, and that the owner can open each tenant's panel.
.github/workflows/smoke.yml runs the same on a clean runner — installing the plugin from the real Packstub registry — on every push, weekly, and on demand, so a new Laravel, Filament, or plugin release that breaks a fresh install shows up here first. To run it in a fork, add a COMPOSER_AUTH repository secret:
{"http-basic":{"packstub.dev":{"username":"pkg_xxxxxxxxxxxxxxxx","password":"your-token-secret"}}}- MySQL / PostgreSQL — set
DB_CONNECTIONand give the DB userCREATE DATABASE; tenant databases are namedtenant{uuid}. - Path identification (
example.com/admin/teams/acme) —'identification' => 'path'inconfig/packstub-tenancy.php; no code changes. - Custom domains, database pools, resource syncing — see the plugin docs.
The hosted demo runs on Laravel Cloud (Starter plan, scales to zero, resets daily). Cloud creates apps from a connected repository, so deploying your own copy is a short dashboard walk-through:
- Fork this repo, then New application → existing repository.
- Database — Add database → Laravel Serverless Postgres (SQLite isn't available on Cloud's ephemeral filesystem). Every tenant gets its own database inside that cluster; the injected
DB_*variables serve as the central connection and the template for tenant databases. - Queue — Add compute → Managed queue (Flex, 256 MiB, 0–1 workers). Provisioning runs here.
- Environment variables — on top of the injected
APP_KEY,DB_*,QUEUE_CONNECTION=cloud:Cloud injectsAPP_NAME="Packstub Tenancy Demo" APP_URL=https://tenancy-demo.example.com DEMO_LOGIN_PREFILL=true # DEMO_RESET_SCHEDULE=true # public demo only: wipes ALL tenants daily (needs the scheduler) PACKSTUB_USER=pkg_xxxxxxxxxxxxxxxx PACKSTUB_SECRET=your-token-secret
SESSION_DRIVER=cookieandCACHE_STORE=databaseby default — changeCACHE_STOREtofileso ordinary page views never wake Serverless Postgres, which is billed per awake hour. The file cache is per-instance and wiped on deploy — fine for a demo that re-seeds anyway. - Commands — Settings → Deployments (Cloud docs on private packages):
The seeder is idempotent — every deploy re-asserts the demo account and both tenants.
# Build composer config http-basic.packstub.dev "$PACKSTUB_USER" "$PACKSTUB_SECRET" composer install --no-dev --optimize-autoloader php artisan optimize # Deploy php artisan migrate --force && php artisan db:seed --force
- Domain — Network → Add domain: wildcard on, no redirect. Add every record Cloud shows (the
_cf-custom-hostnameTXT, the permanent_acme-challengeCNAME that renews the wildcard certificate, and the origin CNAMEs for root and*.) as DNS-only — Cloud fronts the app with its own edge, so proxying breaks certificate issuance.
Running elsewhere? Any host with wildcard TLS, a database user allowed to CREATE DATABASE, and a queue worker will do — see the production guide.
The demo app is MIT (LICENSE.md). packstub/filament-tenancy is commercial software licensed separately by Packstub — questions to support@packstub.dev.