Quickstart
This guide takes you from a fresh Laravel app to your first fully provisioned tenant in about 10 minutes. You'll install Packstub Tenancy, wire up subdomain identification, create a tenant through the onboarding wizard, watch it provision in the background, and land in its panel — with the tenant running on its own database. SQLite is enough for this first run, so no database server setup is required.
Before you start
You need:
- PHP 8.4+ and a Laravel 13 app
- Filament v5 panels installed (
php artisan filament:install --panels) - Your Packstub license credentials
- A local dev domain that resolves wildcard subdomains — Laravel Herd serves
*.myapp.testautomatically
This guide uses myapp.test as the central domain and subdomain identification, which is the recommended mode. Path identification (myapp.test/admin/teams/acme) is covered in Installation.
1. Authenticate with the private registry
Packstub Tenancy is distributed through a private Composer registry. Your Packstub dashboard's Install Guide page shows both commands ready to copy with your access token filled in (the token username starts with pkg_):
composer config repositories.packstub-filament-tenancy composer https://packstub.dev/composer/filament-tenancy
composer config --auth http-basic.packstub.dev pkg_xxxxxxxxxxxxxxxx your-token-secret
See Installation for auth.json details and CI setup.
2. Require the package
composer require packstub/filament-tenancy
This pulls in stancl/tenancy v4 automatically — the registry serves a vetted v4 snapshot as a stable release, so no stability settings or pins are needed in your composer.json (see Installation for how that works).
3. Run the installer
php artisan packstub-tenancy:install
The installer asks how tenants should be identified — choose Subdomain. It then publishes config/packstub-tenancy.php, offers to run the package migrations (tenants, domains, tenant_user, tenant_resources) against your central database — they run straight from the package, no publishing needed (see the run_migrations config key if you'd rather own the schema) — writes your identification choice into the published config, and prints the wiring steps that follow.
4. Review the published stancl/tenancy config
The installer also published config/tenancy.php (stancl/tenancy's config) with the plugin's defaults already applied:
'models' => [
'tenant' => Packstub\Tenancy\Models\Tenant::class, // set for you
// ...
// Leave 'id_generator' at its default (UUIDGenerator): the shipped
// tenants table uses a string primary key, stancl's convention.
],
Bootstrappers\DatabaseSessionBootstrapper is published commented out — stancl's raw config enables it, but sessions and auth are central by design here, and the plugin refuses to boot while it's enabled. See Isolation model for what lives where and why.
The full annotated reference config is in Installation.
5. Update your environment
APP_URL=http://myapp.test
QUEUE_CONNECTION=database
The central domain is parsed from APP_URL (override it with TENANCY_CENTRAL_DOMAIN if they differ). You do not need to set SESSION_DOMAIN: the plugin sets the session cookie domain per request host at runtime — shared .myapp.test cookie on the central domain and tenant subdomains, host-only cookies on custom domains. (If you'd rather manage it yourself, set manage_session_cookie => false in config/packstub-tenancy.php.)
6. Prepare your User model
Users live in the central database and are connected to tenants through the tenant_user pivot. Add the trait and Filament's tenancy contracts:
use Filament\Models\Contracts\HasDefaultTenant;
use Filament\Models\Contracts\HasTenants;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Packstub\Tenancy\Concerns\HasPackstubTenants;
use Stancl\Tenancy\Database\Concerns\CentralConnection;
class User extends Authenticatable implements HasDefaultTenant, HasTenants
{
use CentralConnection; // identity always lives in the central database
use HasPackstubTenants; // canAccessTenant() + getTenants() + getDefaultTenant()
}
HasPackstubTenants filters out tenants that are still provisioning, so users are never dropped into a half-built panel.
7. Register the plugin
In your panel provider (typically app/Providers/Filament/AdminPanelProvider.php), register the plugin. That's it — no stancl middleware to wire. The plugin registers its own central-domain-aware identification middleware globally: it knows your central domain, skips it, identifies tenants by host on subdomains and verified custom domains, sets the right session cookie domain per host, and keeps auth pages on the central origin.
use Packstub\Tenancy\TenancyPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->plugin(TenancyPlugin::make())
->middleware([
// Filament's default middleware stack, unchanged
]);
}
That single TenancyPlugin::make() call wires the tenant model, full-host domain routing (acme.myapp.test — and, if you enable them, custom domains like app.acme-corp.com), the tenant switcher, the onboarding wizard, and the provisioning status page. Every default is configurable — see the fluent API in the configuration reference.
8. Add your tenant migrations
Everything in database/migrations/tenant/ runs against each newly created tenant database:
mkdir -p database/migrations/tenant
Add a migration here for every table your tenants should own. Tenant tables need no tenant_id column — isolation happens at the connection level. Don't copy the users or sessions migrations here: identity, sessions, and auth stay in the central database by design (see Isolation model).
9. Start a queue worker
Provisioning runs on your queue. Without a running worker, new tenants stay on the provisioning screen forever:
php artisan queue:work
In production, keep a worker running under Supervisor or Horizon. Workers must share the app's configuration — they resolve tenant database connections from the same config/database.php.
10. Create your first tenant
Create a user and log in:
php artisan make:filament-user
Visit http://myapp.test/admin/login. Since your user belongs to no tenants yet, Filament sends you straight to the Create Organization wizard (also reachable later at /admin/new). Enter a name — the URL slug fills in when you leave the name field — then confirm on the second step. The tenant row, its subdomain, and your ownership record are committed to the central database in one transaction, and the provisioning pipeline is dispatched to the queue.
What the provisioning screen does
After submitting the wizard you land on /admin/tenant/provisioning/{slug}, a Livewire-polled status page. Behind the scenes, your queue worker runs the pipeline: CreateDatabase → MigrateDatabase → MarkTenantReady (plus SeedDatabase, if you configure a tenant-safe seeder in packstub-tenancy.seeder). The page polls the tenant's status and:
- redirects you into the tenant panel the moment the tenant is ready — usually well under a minute;
- shows a "taking longer than usual" hint after 90 seconds (typically a busy or missing queue worker);
- switches to a failure state after 5 minutes, or immediately if the pipeline fails — with a support link if you set
packstub-tenancy.support_url.
A failed tenant is never left half-broken: the EnsureTenantIsReady middleware redirects every visit to a not-ready tenant back to this page, and php artisan tenants:retry-provisioning {slug} re-queues the full pipeline for a failed tenant — an already-created database is reused, so the retry proceeds straight to migrations.
You're in
Once provisioning finishes, you land at http://acme.myapp.test/admin — the tenant's own panel, backed by the tenant's own database. The tenant switcher in the sidebar shows your organization by name, and /admin/new creates more tenants the same way. Build resources exactly as you would in a single-tenant app: no tenant_id columns, no global scopes — every query in tenant context automatically hits that tenant's database.
Let tenants bring their own domain
Enable custom domains and each tenant can serve their panel from app.their-company.com, with DNS verification and single-origin login handled for you:
TenancyPlugin::make()->customDomains()
Read Custom domains for the verification flow, the cross-domain sign-in handoff, and per-platform TLS notes.
Scale past one database server
When one database server isn't enough, define a connection per server in config/database.php and hand them to the plugin:
TenancyPlugin::make()
->databasePool(['tenant_pool_1', 'tenant_pool_2'], strategy: 'least-tenants')
Every new tenant's database is created on the pool member with the fewest tenants — add a third connection and new tenants start landing there, no other changes needed. Read Horizontal scaling for strategies, weights, monitoring with tenants:pool, and operations.