# How it works

A short tour of the decisions baked into Filament Tenancy: what lives in which database, how a request finds its tenant, and what happens between "Create organization" and a ready panel. You don't need this to install the plugin — read it when you want to know *why* things are wired the way they are.

## Isolation model

Two kinds of database, with a fixed split between them:

| Database | Holds |
|---|---|
| **Central** (one) | `users`, `sessions`, auth, `tenants`, `domains`, the `tenant_user` pivot — everything about *who* and *which workspace* |
| **Tenant** (one per tenant) | Every table in `database/migrations/tenant/` — the workspace's own data |

Tenant isolation happens at the **connection level**: while a request runs in tenant context, the default database connection *is* that tenant's database. Your models and resources need no `tenant_id` column and no global scope — a plain `Project::all()` returns that tenant's projects.

Identity stays central on purpose. Login happens on exactly one origin (the central domain); every host gets its own first-party session cookie; and identity reaches custom domains through a single-use, short-lived handoff code ([Custom domains](https://packstub.dev/docs/filament-tenancy/custom-domains)). That is what makes the tenant switcher, cross-subdomain login, and "logged out everywhere" revocation work — and it's the same model Clerk, Shopify, and Auth0 use now that third-party cookies are gone.

Consequently:

- `DatabaseSessionBootstrapper` stays **out** of `tenancy.bootstrappers`. The installer publishes `config/tenancy.php` with it disabled; if it's ever re-enabled, the plugin strips it at runtime and logs a warning (also visible in `php artisan about`) rather than letting tenant pages fail with `no such table: sessions`.
- `users` and `sessions` migrations don't belong in `database/migrations/tenant/`.

### Which identity model do you need?

"Per-tenant users" means two different things:

| You want… | Use |
|---|---|
| A `users` table **inside each tenant database** — for foreign keys (`posts.user_id`), audit columns, tenant-side relations — while people still sign in once and switch between workspaces | **This plugin, with [resource syncing](https://packstub.dev/docs/filament-tenancy/resource-syncing).** Your central `User` is the sync master; a tenant-side mirror is written into every tenant database the user belongs to and kept in sync. Auth and sessions stay central. This is the common case. |
| Every tenant as its **own auth realm** — separate credentials per tenant, login on each tenant's host, sessions in the tenant database, no shared account | **Plain [stancl/tenancy](https://tenancyforlaravel.com) with Filament's multi-tenancy docs.** That's the model stancl is designed around, and it does it very well. This plugin is built for shared identity, and the two don't mix. |

The tell is the login page: one sign-in that lands you in a workspace picker → this plugin; a different sign-in per tenant host → stancl directly.

## Identification

How a request finds its tenant depends on the mode you chose in the installer. In **both** modes you add no stancl middleware to the panel — the plugin does the identification.

### Subdomain mode

The plugin registers its own central-domain-aware identification middleware in the global stack. On each request it:

1. skips the central domain (parsed from `APP_URL`, or `TENANCY_CENTRAL_DOMAIN` if set);
2. matches tenant hosts — subdomains and verified custom domains — against the `domains` table;
3. sets the session cookie domain for that host: a shared `.myapp.test` cookie on subdomains, a host-only cookie on custom domains (`manage_session_cookie => false` turns this off);
4. keeps auth pages on the central origin.

Filament then resolves the tenant, and the plugin's `TenantSet` bridge initializes stancl tenancy *after* authentication.

> [!WARNING]
> Adding `InitializeTenancyByDomain` or `PreventAccessFromUnwantedDomains` to the panel middleware — the wiring some stancl guides show — breaks the central domain (`TenantCouldNotBeIdentifiedOnDomainException` on login), because stancl's skip logic only applies in the global stack.

### Path mode

Nothing to add either. Filament binds `{tenant}` from the URL and the `TenantSet` bridge does the rest. The installer sets the `PathTenantResolver`'s `tenant_model_column` to `'slug'` in the published `config/tenancy.php` so stancl resolves by the same column Filament binds on. Adding `InitializeTenancyByPath` would throw on the central `/login` route.

## The provisioning pipeline

Creating a tenant — through the onboarding wizard, a seeder, or `TenantOnboarder::create()` — commits the tenant row, its `{slug}.{central-domain}` domain row, and the owner pivot in **one transaction**, then dispatches the pipeline to your queue:

```
CreateDatabase → MigrateDatabase → SeedDatabase (optional) → MarkTenantReady
```

- Until the last job finishes, the tenant's status is `provisioning`. `HasPackstubTenants` hides it from the switcher, and the `EnsureTenantIsReady` middleware routes every visit to a polling status page.
- `SeedDatabase` runs only when `packstub-tenancy.seeder` names a tenant-safe seeder class — never your central `DatabaseSeeder`.
- If a job fails, the status becomes `failed` and the status page says so (with a support link if you set `support_url`). `php artisan tenants:retry-provisioning {slug}` re-queues the pipeline; an already-created database is reused.
- Once `ready`, the switcher lists it and the status page redirects into the panel.

Workers need the same `database.connections` entries as your web servers — provisioning resolves tenant database servers by connection name. See [Production](https://packstub.dev/docs/filament-tenancy/production#queue-workers).

## Where the data goes with a database pool

With a single database server, every tenant database is created on the central connection's server. With a [database pool](https://packstub.dev/docs/filament-tenancy/horizontal-scaling), each new tenant is assigned to one of your servers (by strategy — least tenants, weighted, round-robin) and the assignment is stored on the tenant row, so a tenant's data has one home for its whole life. Add a server, and new tenants start landing there.
