# Troubleshooting

This page lists the problems Packstub Tenancy users hit most often, organized by symptom. Each entry explains what you are seeing, why it happens, and how to fix it. Before digging in, make sure you completed every step in the [installation guide](https://packstub.dev/docs/filament-tenancy/installation) — most issues below are wiring mistakes that the install notes call out. If your problem involves the database pool, also see [Horizontal scaling](https://packstub.dev/docs/filament-tenancy/horizontal-scaling).

## Tenants are stuck on the provisioning screen

**Symptom:** After registering a tenant, the user sits on the "setting up" page indefinitely. After about 90 seconds the page shows a "taking longer than usual" hint, and after 300 seconds it gives up and shows the failed state.

**Cause:** Provisioning (`CreateDatabase` → `MigrateDatabase` → optional `SeedDatabase` → `MarkTenantReady`) runs as a **queued** job pipeline, dispatched after the onboarding transaction commits. If no queue worker is running, the job sits in the queue forever and the tenant never leaves the `provisioning` status. The provisioning page polls every 2 seconds and hard-times-out at 300 seconds as a safety net — a timeout here almost always means the worker is down or wedged.

**Fix:** Run a queue worker in production:

```bash
php artisan queue:work
```

(or supervise it with Horizon / systemd / Supervisor). Once the worker drains the queue, the polling page redirects the user into their panel automatically — the 300-second timeout only affects the page, not the queued job.

If the job actually **failed** (exhausted its retries), the tenant's status flips to `failed` and the page shows the failure state immediately. Fix the underlying cause (check `storage/logs/laravel.log` and your `failed_jobs` table), then re-run the pipeline:

```bash
php artisan tenants:retry-provisioning acme
```

The retry command only accepts tenants in the `failed` status. Retrying is idempotent: an already-created tenant database is reused, and migrations re-run safely.

## Users are bounced to /login after every save (subdomain mode)

**Symptom:** Login works and pages render, but every Livewire interaction — saving a form, running a table action — redirects the user back to the panel login page.

**Cause:** The user's session lives in the tenant database — Stancl's `DatabaseSessionBootstrapper` is active. Sessions and auth are **central** in this plugin's architecture (see [Isolation model](https://packstub.dev/docs/filament-tenancy/installation#isolation-model)); with per-tenant sessions, `StartSession` looks in the wrong database on any request where tenancy isn't initialized first (the login page, the global `/livewire/update` route, …), finds no session, and Filament treats the user as logged out. Note that stancl's raw published config enables this bootstrapper by default — you don't have to have opted in for it to be active. Current plugin versions refuse to boot while it is enabled (a `RuntimeException` naming the fix), and the installer publishes `config/tenancy.php` with it disabled, so this symptom mostly appears on installs that predate the guard. A closely related symptom on such installs is a hard 500 — `no such table: sessions` on the `tenant` connection — on every tenant page.

**Fix:** Remove `DatabaseSessionBootstrapper` from `tenancy.bootstrappers` (and drop any `sessions` table from `database/migrations/tenant/`). If earlier docs led you to add `InitializeTenancyByDomain` / `PreventAccessFromUnwantedDomains` to the panel middleware, remove those too — the plugin's own global identification middleware replaces them (see below).

## 500 on the central login / 404 on every tenant page (subdomain mode)

**Symptom:** The central domain throws `TenantCouldNotBeIdentifiedOnDomainException` on `/admin/login`, or every route on a tenant subdomain 404s.

**Cause:** Stancl's `InitializeTenancyByDomain` and/or `PreventAccessFromUnwantedDomains` were added to the **panel** middleware. `InitializeTenancyByDomain` demands a tenant on every host — including your central domain (its skip logic only applies in the global stack) — and `PreventAccessFromUnwantedDomains` 404s unflagged routes on non-central hosts.

**Fix:** Remove all stancl identification middleware from the panel. Subdomain identification is handled by the plugin's own `IdentifyTenantHost` middleware, registered globally and aware of your `central_domain` — there is nothing to wire.

## RouteIsMissingTenantParameterException (path mode)

**Symptom:** Requests throw `Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException`, typically on `/login` or another central route.

**Cause:** `Stancl\Tenancy\Middleware\InitializeTenancyByPath` was added to the panel middleware. That middleware demands a `{tenant}` parameter on **every** route it runs on, and central panel routes such as `/login` have none. Path mode needs **no** Stancl identification middleware at all: Filament resolves the tenant from the URL itself, and the plugin bridges the resolved tenant into stancl/tenancy automatically (Filament's `TenantSet` event → the plugin's `SyncStanclTenancy` listener).

**Fix:** Remove `InitializeTenancyByPath` (and any other Stancl identification middleware) from your panel. In path mode the plugin's bridge is the identification mechanism.

## 404 on tenant routes (path mode)

**Symptom:** In path mode, links into the tenant area — or redirects after login — produce 404s, often with the tenant's **ID** in the URL where the slug should be.

**Cause:** The plugin registers Filament's tenant route binding on the `slug` attribute, so panel URLs look like `/admin/teams/acme`. Stancl's path machinery (the `PathTenantResolver`, and the `UrlGeneratorBootstrapper`'s URL defaults applied through the `TenancyUrlGenerator` override) fills the `{tenant}` parameter from its `tenant_model_column` setting — which defaults to the tenant's primary key. With the default in place, Stancl-generated URLs carry the tenant ID, and Filament's slug binding cannot resolve them.

**Fix:** Point the resolver at the slug in `config/tenancy.php`:

```php
'identification' => [
    'resolvers' => [
        Stancl\Tenancy\Resolvers\PathTenantResolver::class => [
            // ...
            'tenant_model_column' => 'slug',
        ],
    ],
],
```

## Boot exception: "packstub/filament-tenancy requires tenancy.database.central_connection"

**Symptom:** The application throws at boot:

```
packstub/filament-tenancy requires tenancy.database.central_connection to be set to your
central database connection name.
```

**Cause:** `tenancy.database.central_connection` is null or empty. The plugin pins all central writes — the tenant model, onboarding, profile updates, resource-syncing masters — to this connection. Without it, those writes would silently fall through to whatever connection is active, which inside a tenant context is the **tenant** database. The plugin refuses to boot rather than risk corrupting data.

**Fix:** Set the central connection in `config/tenancy.php`:

```php
'database' => [
    'central_connection' => env('DB_CONNECTION', 'central'),
    // ...
],
```

Make sure `DB_CONNECTION` is set in your environment, or hardcode your central connection's name.

## Provisioning fails with an SQLSTATE error on CREATE DATABASE

**Symptom:** Tenants land in the `failed` status, and the failed job's exception is an SQLSTATE error from a `CREATE DATABASE` statement (permission denied, unknown database, connection refused).

**Cause:** A pool member (or your template tenant connection) cannot create databases. Each connection used for tenant databases must define the server's host and port, **admin credentials with the create-database privilege**, and an **existing maintenance database** to connect to — provisioning connects to that database first, then issues `CREATE DATABASE` from there.

**Fix:** Probe every pool member:

```bash
php artisan tenants:pool --check
```

The check connects to each member and verifies it can create tenant databases — on PostgreSQL it confirms the role has `CREATEDB` (or superuser), on MySQL/MariaDB a global `CREATE` grant. Grant the missing privilege:

```sql
-- PostgreSQL
ALTER ROLE tenants_a CREATEDB;

-- MySQL / MariaDB
GRANT CREATE ON *.* TO 'tenants_a'@'%';
```

Also confirm the `database` field of the connection names a database that already exists on that server. Then retry the affected tenants with `php artisan tenants:retry-provisioning {slug}`.

## tenants:pool exits 1 and reports stranded tenants

**Symptom:** `php artisan tenants:pool` exits with code 1 and prints:

```
2 tenant(s) point at connections missing from config/database.php — they cannot boot:
  acme -> tenant_pool_b
  globex -> tenant_pool_b
```

**Cause:** A connection that tenants were assigned to was renamed or removed from `config/database.php`. Each tenant stores its server as a persisted `tenancy_db_connection` attribute; if that name no longer resolves to a connection, the tenant cannot boot — provisioning, runtime access, and deletion all fail.

**Fix:** Either restore the connection definition under its original name, or repoint each stranded tenant at an existing member:

```php
$tenant = \Packstub\Tenancy\Models\Tenant::where('slug', 'acme')->first();
$tenant->setInternal('db_connection', 'tenant_pool_a');
$tenant->save();
```

Only repoint a tenant at a server that actually holds its database. If the data lives elsewhere, move it first (dump and restore the tenant database onto the target server), then update the attribute — there is no automatic tenant-move primitive. Treat renaming pool connections as a breaking change.

## Queue workers throw DatabaseManagerNotRegisteredException (or a TypeError about the connection config)

**Symptom:** Provisioning jobs fail on your queue workers with `Stancl\Tenancy\Database\Exceptions\DatabaseManagerNotRegisteredException`, or with a `TypeError` complaining that the template connection config is null — while the same operations work on the web servers.

**Cause:** Provisioning runs on the queue, so the **worker** process resolves the tenant's server from its `tenancy_db_connection` attribute — against the worker's own configuration. If the worker environment is missing the pool connection definitions (unset environment variables, an out-of-date deployment, a stale config cache), the lookup returns nothing: a missing connection surfaces as the null-config `TypeError`, and a connection whose driver has no entry in `tenancy.database.managers` throws `DatabaseManagerNotRegisteredException`.

**Fix:** Deploy identical `config/database.php` connections (and their environment variables) everywhere workers run, then clear the config cache and restart the workers so they pick up the new configuration:

```bash
php artisan config:clear
php artisan queue:restart
```

## A tenant sees wrong or empty data after the pool was enabled

**Symptom:** You enabled the database pool on an app with existing tenants. New tenants work, but a pre-existing tenant now sees empty tables or another dataset entirely.

**Cause:** The pool assigns a server **only to new tenants**, at creation time. Existing tenants have no persisted `tenancy_db_connection` and keep following Stancl's `tenancy.database.template_tenant_connection` (or the central connection) — `tenants:pool` shows them as the `(template default)` row. If you repointed that template connection at a pool server when enabling the pool, those tenants now connect to a server that does not hold their databases: Stancl connects to whatever database matches the tenant's database name there, which is either missing or freshly empty.

**Fix:** Leave the template connection pointing at the server where the existing tenant databases actually live; the pool balances new tenants independently. To move an existing tenant onto a pool member, dump and restore its database to the target server, then pin it explicitly:

```php
$tenant->setInternal('db_connection', 'tenant_pool_b');
$tenant->save();
```

Explicitly pinned tenants are never reassigned by the pool.

## Every provision fails at the seed step

**Symptom:** Every tenant registration fails, and the failed job's stack trace points at a seeder — typically referencing tables or models that only exist in the **central** database.

**Cause:** Tenant seeding ran with a seeder that is not tenant-safe. Your application's `Database\Seeders\DatabaseSeeder` seeds the central schema; run against a fresh tenant database it fails — or worse, silently misbehaves. This is why tenant seeding is **opt-in**: with `packstub-tenancy.seeder` left `null` (the default), the pipeline skips the seed step entirely.

**Fix:** Write a seeder that only touches tenant-schema tables and opt in to it:

```php
// config/packstub-tenancy.php
'seeder' => Database\Seeders\TenantSeeder::class,
```

The plugin runs it with `--force`, so non-interactive queue workers never prompt. If you don't need seeded data, set `'seeder' => null` and retry the failed tenants.

## RuntimeException: queued resource syncing needs the QueueTenancyBootstrapper

**Symptom:** The application throws at boot:

```
Resource syncing is configured to queue (queueResourceSync), but
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper is not enabled in
tenancy.bootstrappers.
```

**Cause:** You called `->queueResourceSync()`, but Stancl's `QueueTenancyBootstrapper` is not in `tenancy.bootstrappers`. That bootstrapper is what restores the originating tenant (or central) database context inside a queue worker. Without it, a queued sync job runs against whatever connection the worker happens to have — silently reading and writing the wrong database — so the plugin fails loudly instead.

**Fix:** Enable the bootstrapper in `config/tenancy.php`:

```php
'bootstrappers' => [
    // ...
    Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
],
```

or run the sync listeners inline with `->queueResourceSync(false)`.

## Resources error about a missing tenant_id column

**Symptom:** Opening a Filament resource inside a tenant throws an SQL error such as `SQLSTATE[42703]: Undefined column: column "tenant_id" does not exist`.

**Cause:** `scope_resources_to_tenant` is `true` (or you called `->scopeResourcesToTenant()`). That setting makes Filament add a tenant-ownership constraint to every resource query — which expects a `tenant_id` column on your tables. In multi-database tenancy no such column exists: each tenant's tables live in their own database, and Stancl's `DatabaseTenancyBootstrapper` already isolates tenants at the connection level.

**Fix:** Keep the setting at its default:

```php
// config/packstub-tenancy.php
'scope_resources_to_tenant' => false,
```

Only enable it for single-database setups where tenant rows share tables and genuinely carry a `tenant_id` discriminator column.

## Still stuck?

Set `support_url` in `config/packstub-tenancy.php` so your own users land on your support channel when provisioning fails. For issues with the plugin itself, email [support@packstub.dev](mailto:support@packstub.dev) — bug reports from license holders are answered directly, and fixes ship as regular releases.
