# Configuration

Packstub Tenancy is configured in two equivalent ways: the published `config/packstub-tenancy.php` file, and a fluent API on `TenancyPlugin` that mirrors every panel-relevant config key. Fluent calls always win over the config file — the plugin resolves each key from its own overrides first and falls back to config — so you can keep app-wide defaults in the file and override per panel in code. This page is the complete reference: the full annotated config file, the config-key ↔ fluent-method map, and a section on every key.

## The configuration file

Publishing happens during `php artisan packstub-tenancy:install` (or manually with `php artisan vendor:publish --tag=packstub-tenancy-config`). This is the full shipped file:

```php
<?php

use Packstub\Tenancy\Filament\Billing\NullBillingProvider;
use Packstub\Tenancy\Filament\Pages\EditPackstubTenantProfile;
use Packstub\Tenancy\Filament\Pages\OnboardTenant;
use Packstub\Tenancy\Models\Tenant;

return [
    'tenant_model' => Tenant::class,

    /*
    | Whether the package's own migrations run automatically (zero-config default).
    |
    | Set to false when you need to adapt the schema to your app — e.g. extra
    | columns on tenants, different cascade rules, or an integer tenant key.
    | Then publish the migrations and edit your copies:
    |
    |   php artisan vendor:publish --tag=packstub-tenancy-migrations
    |
    | Never leave this true after publishing: both copies would register as
    | pending migrations and `migrate` would try to create each table twice.
    | Note: with auto-run disabled you own the schema — future plugin updates
    | that change it will ship upgrade notes instead of applying automatically.
    */
    'run_migrations' => true,

    /*
    | How tenant DATA is separated — the master switch several keys below
    | follow.
    |
    | - 'dedicated' (default): one database per tenant, provisioned on signup
    |   by the queued CreateDatabase/MigrateDatabase pipeline. Isolation is the
    |   connection switch itself; tenant tables carry no tenant_id column.
    |   With the database pool enabled, pool members are database SERVERS and
    |   each new tenant's database is created on one of them.
    |
    | - 'shared': tenants share pre-provisioned databases and are isolated by
    |   a tenant_id relationship scope (scope_resources_to_tenant defaults to
    |   true). No per-tenant database is created — tenants are ready almost
    |   instantly. Rows live in the central database by default; with the
    |   database pool enabled, pool members are shared SHARD databases (each an
    |   ordinary connection whose `database` already exists and is migrated)
    |   and new tenants are load-balanced across them.
    |
    | Per-tenant override for hybrid fleets: create a tenant with an
    | `isolation_mode` attribute ('dedicated' | 'shared') to give it the other
    | model — e.g. a private database for an enterprise/data-residency tenant
    | inside a shared app. See docs/database-strategies.md.
    */
    'database_strategy' => 'dedicated',

    'central_domain' => env('TENANCY_CENTRAL_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),

    /*
    | How a request is matched to a tenant. NO stancl middleware to wire in
    | either mode — the plugin identifies tenants itself.
    | - 'subdomain': full-host domain routing (`{tenant:route_host}`): the
    |                request host is matched against VERIFIED domain rows,
    |                covering `{slug}.central_domain` subdomains and (when
    |                enabled) custom domains.
    | - 'path':      panel is wired with `->tenantRoutePrefix(route_prefix)`;
    |                tenants live at /{panel}/{route_prefix}/{slug}.
    */
    'identification' => 'subdomain',

    'route_prefix' => 'teams',

    /*
    | Seeder run inside each freshly provisioned tenant database.
    |
    | Tenant seeding is OPT-IN. Leave this null to skip the seed step entirely
    | (the safe default): Stancl's global `tenancy.seeder_parameters` points at
    | the CENTRAL Database\Seeders\DatabaseSeeder, which references central-only
    | tables/models and would fail — or worse, silently misbehave — against a
    | fresh tenant DB. Set this to a tenant-safe seeder class to enable seeding;
    | the plugin runs it with --force so queued (non-interactive) workers don't
    | prompt in production.
    */
    'seeder' => null,

    /*
    | The relationship name on tenant-aware resources that points to the tenant.
    | Leave null to use Filament's default (camelCased basename of the tenant model).
    | Mostly relevant when resources are scoped to the tenant (shared strategy).
    */
    'ownership_relationship' => null,

    /*
    | Whether Filament should automatically scope resources to the current
    | tenant through the ownership relationship (whereBelongsTo($tenant)).
    |
    | Default null = follow database_strategy: false under 'dedicated', where
    | Stancl's DatabaseTenancyBootstrapper already isolates each tenant on its
    | own connection and tenant-DB models have no tenant relationship to scope
    | by; true under 'shared', where the tenant_id relationship scope IS the
    | isolation. Set an explicit bool to override either way.
    */
    'scope_resources_to_tenant' => null,

    'menu' => [
        'enabled' => true,
        'switcher_enabled' => true,
        'searchable' => true,
        // Array of \Filament\Actions\Action | \Filament\Navigation\MenuItem | Closure
        // forwarded verbatim to Panel::tenantMenuItems().
        'items' => [],
    ],

    'onboarding' => [
        'enabled' => true,
        'page' => OnboardTenant::class,
    ],

    'profile' => [
        'enabled' => false,
        'page' => EditPackstubTenantProfile::class,
    ],

    'billing' => [
        'enabled' => false,
        'provider' => NullBillingProvider::class,
        'route_slug' => 'billing',
        'required' => false,
    ],

    'middleware' => [
        // Extra persistent middleware appended after EnsureTenantIsReady.
        'extra' => [],
    ],

    /*
    | Horizontal scaling: load-balance new tenant databases across multiple
    | database servers.
    |
    | Each entry in `connections` is the name of an ordinary connection from
    | config/database.php that points at a tenant database server — its host,
    | port, admin credentials (CREATE DATABASE privilege), and an existing
    | maintenance database on that server. When a tenant is created, the plugin
    | picks a member using `strategy` and persists it as the tenant's
    | `tenancy_db_connection`; Stancl then provisions, migrates, connects, and
    | deletes that tenant on its own server — no further routing needed.
    |
    | Strategies:
    |   - 'least-tenants' (default): member with the fewest tenants; self-heals
    |     after deletions and after a new server joins the pool.
    |   - 'round-robin': rotate by total pooled-tenant count.
    |   - 'weighted': fewest tenants per weight unit — give bigger servers a
    |     larger `weights` entry (missing weights count as 1).
    |
    | To scale out: add a connection in config/database.php, append its name
    | here, deploy (workers too — provisioning runs on the queue). Tenants
    | with an explicit `tenancy_db_connection` (e.g. data-residency pins) are
    | never reassigned.
    */
    'database_pool' => [
        'enabled' => false,
        'connections' => [],
        'strategy' => 'least-tenants',
        'weights' => [],
    ],

    /*
    | Shown on the provisioning page when a tenant's setup fails, so users
    | land on YOUR support channel. Any URL (https://…, mailto:…).
    */
    'support_url' => null,

    /*
    | In subdomain mode the plugin's global IdentifyTenantHost middleware sets
    | the session cookie domain per request host: `.central-domain` on the
    | central domain and tenant subdomains (one shared session), host-only on
    | verified custom domains. That removes the SESSION_DOMAIN env wiring from
    | installs. Set to false to manage `session.domain` yourself.
    */
    'manage_session_cookie' => true,

    /*
    | Custom domains per tenant (subdomain mode only).
    |
    | When enabled, tenants can attach their own domains (app.acme-corp.com).
    | Each domain must pass DNS TXT verification before it identifies the
    | tenant. Authentication on custom domains uses a central-login handoff:
    | a single-use, short-TTL code minted on the central domain and exchanged
    | on the custom domain for a host-only session cookie.
    */
    'custom_domains' => [
        'enabled' => false,
        'interstitial' => false,
        'handoff_ttl' => 60,
        'handoff_path' => 'auth/handoff',
        'verification_prefix' => '_packstub-verify',
    ],

    /*
    | Cross-database resource syncing (powered by Stancl\Tenancy\ResourceSyncing).
    |
    | When enabled, models marked with the SyncsToTenants and IsTenantResource
    | traits keep their `synced_attributes` mirrored between the central DB and
    | every attached tenant DB.
    |
    | The plugin equivalents (preferred) are:
    |   ->syncResources([CentralUser::class => TenantUser::class])
    |   ->queueResourceSync()
    |   ->cleanupOrphanedResourceMappings()
    */
    'resource_syncing' => [
        'enabled' => false,
        'pairs' => [],
        'queue' => false,
        // false | true (default tenant_resources pivot) | ['table' => 'tenant_id_column', ...]
        'cleanup' => false,
    ],
];
```

## Config keys and fluent methods

Every panel-relevant key has a fluent counterpart on `TenancyPlugin`. A fluent call overrides the config value wholesale for that panel — it does not merge with it. Keys marked "config only" have no fluent method because they are consumed outside the panel (by the service provider, provisioning jobs, or Blade views).

| Config key | Fluent method |
|---|---|
| `tenant_model` | — (config only) |
| `run_migrations` | — (config only) |
| `database_strategy` | `->databaseStrategy('dedicated' \| 'shared')` |
| `central_domain` | `->centralDomain('example.com')` |
| `identification` | `->identification('subdomain' \| 'path')` |
| `route_prefix` | `->routePrefix('teams')` |
| `seeder` | — (config only) |
| `ownership_relationship` | `->ownershipRelationship('team')` |
| `scope_resources_to_tenant` | `->scopeResourcesToTenant()` |
| `menu.enabled` | `->withTenantMenu()` |
| `menu.switcher_enabled` | `->withTenantSwitcher()` |
| `menu.searchable` | `->searchableTenantMenu()` |
| `menu.items` | `->tenantMenuItems([...])` |
| `onboarding.enabled` + `onboarding.page` | `->withTenantRegistration(Page::class)` — pass `null` to disable |
| `profile.enabled` + `profile.page` | `->withTenantProfile(Page::class)` — no argument keeps the shipped page |
| `billing.*` | `->withTenantBilling(Provider::class, routeSlug: 'billing', required: false)` |
| `middleware.extra` | `->extraTenantMiddleware([...])` |
| `database_pool.*` | `->databasePool([...], strategy: '...', weights: [...])` — `[]` disables |
| `support_url` | — (config only) |
| `manage_session_cookie` | — (config only) |
| `custom_domains.enabled` | `->customDomains()` |
| `custom_domains.interstitial` | `->handoffInterstitial()` |
| `custom_domains.handoff_ttl` / `handoff_path` / `verification_prefix` | — (config only) |
| `resource_syncing.enabled` + `resource_syncing.pairs` | `->syncResources([Central::class => Tenant::class])` |
| `resource_syncing.queue` | `->queueResourceSync()` |
| `resource_syncing.cleanup` | `->cleanupOrphanedResourceMappings()` |

A typical fluent setup in your panel provider:

```php
use Packstub\Tenancy\TenancyPlugin;

$panel->plugin(
    TenancyPlugin::make()
        ->identification('path')
        ->routePrefix('teams')
        ->databasePool(['tenant_pool_1', 'tenant_pool_2'], strategy: 'least-tenants')
        ->withTenantProfile()
        ->extraTenantMiddleware([\App\Http\Middleware\AuditTenantAccess::class]),
);
```

One exception to "fluent is panel-local": `->databasePool(...)` writes its values back into `config('packstub-tenancy.database_pool')` when the panel boots, so the tenant-creation hook, queued provisioning jobs, and the `tenants:pool` command all see the same pool the panel was configured with. `->databaseStrategy(...)` is mirrored the same way, for the same reason — tenant creation defaults, pool placement, and bootstrapper routing all read it outside the panel.

## `tenant_model`

**Default:** `Packstub\Tenancy\Models\Tenant`

The Eloquent model Filament registers as the panel tenant (bound by its `slug` attribute) and the model the provisioning pipeline, database pool, and `HasPackstubTenants` trait operate on.

The shipped model extends Stancl's base tenant and already satisfies everything the plugin needs. If you bring your own model, it must meet the same contract:

- implement `Filament\Models\Contracts\HasName`, `HasAvatar`, and `HasCurrentTenantLabel` — so the tenant switcher shows a real name, avatar (read from the `avatar_url` column), and "active tenant" label
- implement `Stancl\Tenancy\Database\Contracts\TenantWithDatabase` and use the `HasDatabase` and `HasDomains` traits — so per-tenant databases and subdomain identification work
- use the plugin's `Packstub\Tenancy\Concerns\IsPackstubTenant` trait, which supplies the plugin-specific pieces: the provisioning `status` lifecycle, slug-based route binding, the `users()` membership relationship, and the Filament name/avatar/label accessors

```php
use Packstub\Tenancy\Concerns\IsPackstubTenant;
use Stancl\Tenancy\Database\Models\Tenant as StanclTenant;

class Organization extends StanclTenant implements
    \Filament\Models\Contracts\HasAvatar,
    \Filament\Models\Contracts\HasCurrentTenantLabel,
    \Filament\Models\Contracts\HasName,
    \Stancl\Tenancy\Database\Contracts\TenantWithDatabase
{
    use \Stancl\Tenancy\Database\Concerns\HasDatabase;
    use \Stancl\Tenancy\Database\Concerns\HasDomains;
    use IsPackstubTenant;
}
```

Then point the config at it:

```php
'tenant_model' => \App\Models\Organization::class,
```

There is no fluent method for this key: the service provider registers the database-pool `creating` hook on the model before any panel boots, so the model must be known from config.

## `run_migrations`

**Default:** `true`

Whether the package's own central-database migrations (`tenants`, `tenant_user`, `domains`) run automatically. The default gives you a zero-config install and lets plugin updates ship schema changes with no effort on your side; the shipped migrations already adapt to string tenant keys and your user model's key type.

Set it to `false` when you need to own the schema — extra columns on `tenants`, different cascade rules, an integer tenant key — then publish the migrations and edit your copies:

```bash
php artisan vendor:publish --tag=packstub-tenancy-migrations
php artisan migrate
```

Never leave it `true` after publishing: both copies would register as pending migrations and `migrate` would try to create each table twice. With auto-run disabled you own the schema — future releases that change it ship upgrade notes in the changelog instead of applying automatically.

There is no fluent method for this key: migrations are loaded by the service provider before any panel boots.

## `database_strategy`

**Default:** `'dedicated'`
**Fluent:** `->databaseStrategy('dedicated' | 'shared')`

How tenant data is separated — the master switch several other keys follow:

- **`'dedicated'`** — one database per tenant, provisioned on signup by the queued `CreateDatabase → MigrateDatabase → MarkTenantReady` pipeline. Isolation is the connection switch itself; tenant tables carry no `tenant_id` column. With the [database pool](https://packstub.dev/docs/filament-tenancy/horizontal-scaling) enabled, pool members are database *servers* and each new tenant's database is created on one of them.
- **`'shared'`** — tenants share pre-provisioned databases and are isolated by a `tenant_id` relationship scope (`scope_resources_to_tenant` defaults to `true`), optionally hardened with PostgreSQL Row-Level Security via stancl's `tenants:rls` toolchain. No per-tenant database is created, so tenants are ready almost instantly. Rows live in the central database by default; with the database pool enabled, pool members are shared *shard* databases and new tenants are load-balanced across them.

Individual tenants can override the app-wide strategy at creation with an `isolation_mode` attribute (`'dedicated' | 'shared'`), which is what makes hybrid fleets possible — a private database for one enterprise tenant inside a shared app, or a shared free tier inside a dedicated app.

The full setup for each model — schema, scoping, shards, promotion between modes — is covered in [Database strategies](https://packstub.dev/docs/filament-tenancy/database-strategies).

## `central_domain`

**Default:** `env('TENANCY_CENTRAL_DOMAIN')`, falling back to the host of `APP_URL`
**Fluent:** `->centralDomain('example.com')`

The apex domain tenant subdomains hang off in subdomain mode. With `central_domain` set to `example.com`, the tenant `acme` lives at `acme.example.com`, and the plugin's identification middleware treats `example.com` as the central (login/onboarding) origin. Set it explicitly via the `TENANCY_CENTRAL_DOMAIN` environment variable when your `APP_URL` host is not the tenant apex, or per panel with the fluent `->centralDomain()`. Ignored in path mode.

## `identification`

**Default:** `'subdomain'` — the installer prompt writes your choice into the published config.

How a request is matched to a tenant:

- **`'subdomain'`** — the panel is wired with full-host domain routing (`->tenantDomain('{tenant:route_host}')`): the whole request host is matched against the tenant's **verified** domain rows, which covers `acme.{central_domain}` subdomains and, when enabled, [custom domains](https://packstub.dev/docs/filament-tenancy/custom-domains). No stancl middleware to add — the plugin registers its own identification middleware globally.
- **`'path'`** — the panel is wired with `->tenantRoutePrefix(route_prefix)`, so tenants live at `example.com/admin/{prefix}/{slug}`. Do **not** add `InitializeTenancyByPath` to the panel: it throws on tenant-less central routes such as `/login`. The plugin bridges Filament's resolved tenant into stancl/tenancy automatically (via the `TenantSet` event).

The two modes need different surrounding setup (middleware, sessions, `config/tenancy.php`) — see [Installation](https://packstub.dev/docs/filament-tenancy/installation) for the mode-specific checklists.

## `route_prefix`

**Default:** `'teams'`

The URL segment placed before the tenant slug in path mode: `example.com/admin/teams/acme`. Ignored in subdomain mode.

## `seeder`

**Default:** `null` (no seeding)

The seeder class run inside each freshly provisioned tenant database. Tenant seeding is **opt-in**, and the default is deliberately `null`: Stancl's global `tenancy.seeder_parameters` points at your app's *central* `Database\Seeders\DatabaseSeeder`, which references central-only tables and would fail — or silently misbehave — against a fresh tenant database, turning every provision into a failure.

When you set a tenant-safe seeder class:

```php
'seeder' => \Database\Seeders\TenantSeeder::class,
```

the plugin inserts a `SeedDatabase` step into the provisioning pipeline (`CreateDatabase → MigrateDatabase → SeedDatabase → MarkTenantReady`) and runs the seeder with `--force`, so queued, non-interactive workers never hang on a production prompt.

## `ownership_relationship`

**Default:** `null` (Filament's default — the camelCased basename of the tenant model)

The relationship name on tenant-aware resources that points at the tenant, passed through to Filament's `->tenant()` registration. Only meaningful when resources are scoped to the tenant (the shared strategy, or an explicit `scope_resources_to_tenant => true`); under the dedicated strategy resources are not scoped by relationship at all.

## `scope_resources_to_tenant`

**Default:** `null` (follow `database_strategy`)

Whether Filament automatically scopes resource queries to the current tenant through the ownership relationship (`whereBelongsTo($tenant)`).

The default `null` follows the [database strategy](#database_strategy): `false` under `'dedicated'`, where Stancl's `DatabaseTenancyBootstrapper` already isolates each tenant on its own database connection — and tenant-DB models have no tenant relationship to scope by, so scoping would break every resource query. `true` under `'shared'`, where the `tenant_id` relationship scope *is* the isolation — leaving it off there would show every tenant's rows to everyone.

Set an explicit bool only when you need to override the strategy default — e.g. a shared-strategy app that scopes with its own global scopes instead of Filament's.

## `menu`

**Defaults:** `enabled: true`, `switcher_enabled: true`, `searchable: true`, `items: []`

Controls Filament's tenant menu in the topbar:

- `enabled` — show or hide the tenant menu entirely (`->withTenantMenu(false)` to hide).
- `switcher_enabled` — show or hide the tenant switcher inside the menu (`->withTenantSwitcher(false)`).
- `searchable` — whether the switcher offers a search input; useful for users who belong to many tenants (`->searchableTenantMenu()`).
- `items` — an array of `Filament\Actions\Action`, `Filament\Navigation\MenuItem`, or `Closure` entries, forwarded verbatim to `Panel::tenantMenuItems()`. Because closures don't belong in cached config files, prefer the fluent form:

```php
use Filament\Actions\Action;

TenancyPlugin::make()
    ->tenantMenuItems([
        Action::make('settings')
            ->url(fn (): string => route('filament.admin.tenant.settings'))
            ->icon('heroicon-m-cog-8-tooth'),
    ]);
```

## `onboarding`

**Defaults:** `enabled: true`, `page: Packstub\Tenancy\Filament\Pages\OnboardTenant::class`

The tenant registration (onboarding) page — where a user creates a new tenant, which kicks off queued database provisioning and lands them on the Livewire-polled provisioning screen. `page` must be a `Filament\Pages\Tenancy\RegisterTenant` subclass.

Fluently, one method covers both keys — pass your page class to replace the shipped page, or `null` to disable registration:

```php
TenancyPlugin::make()->withTenantRegistration(App\Filament\Pages\RegisterOrganization::class);

TenancyPlugin::make()->withTenantRegistration(null); // no self-service tenant creation
```

## `profile`

**Defaults:** `enabled: false`, `page: Packstub\Tenancy\Filament\Pages\EditPackstubTenantProfile::class`

The tenant profile page — where members edit the current tenant's name and details. Disabled by default. `page` must be a `Filament\Pages\Tenancy\EditTenantProfile` subclass.

```php
TenancyPlugin::make()->withTenantProfile(); // enable with the shipped page

TenancyPlugin::make()->withTenantProfile(App\Filament\Pages\EditOrganizationProfile::class);
```

## `billing`

**Defaults:** `enabled: false`, `provider: NullBillingProvider::class`, `route_slug: 'billing'`, `required: false`

Wires Filament's tenant billing integration:

- `provider` — a `Filament\Billing\Providers\Contracts\BillingProvider` class (or instance, fluently). The shipped `NullBillingProvider` is a stub that renders a placeholder page; replace it with your real integration.
- `route_slug` — the URL slug of the billing route within the tenant panel.
- `required` — when `true`, the panel calls `requiresTenantSubscription()`, which appends the provider's *subscribed middleware* to every tenant route so unsubscribed tenants are redirected to billing.

```php
TenancyPlugin::make()->withTenantBilling(
    App\Billing\StripeBillingProvider::class,
    routeSlug: 'billing',
    required: true,
);
```

**Guard:** if `required` is `true` but the provider's `getSubscribedMiddleware()` returns an empty string, registration throws a `LogicException` instead of shipping broken routes — Laravel would otherwise try to resolve `''` as a middleware class and 500 on every tenant page. In practice this means the stub `NullBillingProvider` cannot be used with required billing: a required provider must return the middleware class that actually enforces the subscription.

## `middleware.extra`

**Default:** `[]`

Extra middleware classes appended to the panel's *persistent* tenant middleware, after the plugin's `EnsureTenantIsReady` (which keeps users out of tenants that are still provisioning or failed). Persistent tenant middleware also runs on Livewire update requests, so this is the right place for anything that must hold on every tenant interaction — auditing, feature gating, and the like.

```php
TenancyPlugin::make()->extraTenantMiddleware([
    App\Http\Middleware\AuditTenantAccess::class,
]);
```

## `database_pool`

**Defaults:** `enabled: false`, `connections: []`, `strategy: 'least-tenants'`, `weights: []`

Horizontal scaling: load-balance new tenant databases across multiple database servers. Each entry in `connections` names an ordinary connection from `config/database.php` that points at a tenant database server — host, port, admin credentials with the `CREATE DATABASE` privilege, and an existing maintenance database on that server. At tenant creation the plugin picks a member using `strategy` and persists it as the tenant's `tenancy_db_connection`; Stancl then provisions, migrates, connects, and deletes that tenant on its own server with no further routing.

```php
TenancyPlugin::make()->databasePool(
    ['tenant_pool_1', 'tenant_pool_2'],
    strategy: 'least-tenants',
);
```

Strategies: `least-tenants` (default), `round-robin`, and `weighted` (pass `weights: ['tenant_pool_1' => 2, 'tenant_pool_2' => 1]` to give bigger servers more tenants; missing weights count as 1). Tenants with an explicit `tenancy_db_connection` — for example data-residency pins — are never reassigned by the pool. The pool configuration is validated at boot: undefined or reserved connection names, URL-style connections, a missing database manager for a connection's driver, an unknown strategy, or invalid weights all throw immediately rather than failing at provisioning time.

Monitor the pool with `php artisan tenants:pool` (add `--check` for a CI-friendly health exit code) or the `Packstub\Tenancy\Filament\Widgets\DatabasePoolOverview` dashboard widget.

See [Horizontal scaling](https://packstub.dev/docs/filament-tenancy/horizontal-scaling) for the full guide — server prerequisites, scaling out, and operational caveats.

## `support_url`

**Default:** `null`

A URL shown on the provisioning page when a tenant's setup fails, so stuck users land on *your* support channel instead of a dead end. Any URL works — `https://support.example.com`, `mailto:support@example.com`. When `null`, the failure state shows a generic "contact support" message without a link.

## `manage_session_cookie`

**Default:** `true`

In subdomain mode the plugin's identification middleware sets the session cookie domain per request host — `.central-domain` on the central domain and every tenant subdomain (one shared login), host-only on verified custom domains — and mirrors it into the cookie jar so remember-me cookies land on the same domain. This is what removes the `SESSION_DOMAIN` env wiring from installs. Set to `false` to take back manual control of `session.domain`; identification and hygiene redirects keep working either way. No fluent method: the middleware runs globally, outside any panel.

## `custom_domains`

**Default:** disabled
**Fluent:** `->customDomains()`, `->handoffInterstitial()`

Lets tenants attach their own DNS-verified domains, served with full-host routing and central-login code handoff. Subdomain mode only — enabling it in path mode throws at registration. The sub-keys:

- `enabled` — turns on the Domains page, the `tenants:domains` command, custom-domain identification, and the handoff endpoints.
- `interstitial` — show a "Continue as …" confirmation on the custom domain instead of establishing the handed-off session silently.
- `handoff_ttl` — seconds a handoff code stays redeemable (default 60, hard-capped at 120).
- `handoff_path` — path of the landing/exchange endpoint on tenant hosts (default `auth/handoff`).
- `verification_prefix` — DNS TXT record name prefix for ownership proof (default `_packstub-verify`).

The full feature — verification flow, handoff security model, logout propagation, TLS notes — is documented in [Custom domains](https://packstub.dev/docs/filament-tenancy/custom-domains).

## `resource_syncing`

**Defaults:** `enabled: false`, `pairs: []`, `queue: false`, `cleanup: false`

Cross-database resource syncing, powered by `Stancl\Tenancy\ResourceSyncing`. When enabled, models marked with the plugin's `SyncsToTenants` (central) and `IsTenantResource` (tenant) traits keep their synced attributes (declared via a `syncedAttributes()` method or `$syncedAttributes` property on the model) mirrored between the central database and every attached tenant database — the canonical example is one central user identity reflected into each tenant DB the user belongs to.

- `enabled` — turns the syncing listeners on.
- `pairs` — a map of central model class ⇒ tenant model class. Pairs are informational at registration time (Stancl's listeners react to the contracts on the models themselves), but declaring them enables an extra integrity check at boot and makes the wiring explicit.
- `queue` — push the syncing listeners onto the queue; useful when one central change fans out to many tenant databases and blocking the request is undesirable.
- `cleanup` — configure the listener that deletes a tenant's pivot mappings when the tenant is deleted. `false` disables it, `true` uses Stancl's default polymorphic `tenant_resources` pivot (the one this plugin ships), or pass `['table' => 'tenant_id_column']` for additional basic pivots.

The fluent equivalents are preferred in panel apps:

```php
TenancyPlugin::make()
    ->syncResources([App\Models\User::class => App\Models\TenantUser::class])
    ->queueResourceSync()
    ->cleanupOrphanedResourceMappings();
```

The config keys exist so non-Filament entry points — central-only Artisan workers, for instance — get the same wiring without a panel booting. See [Resource syncing](https://packstub.dev/docs/filament-tenancy/resource-syncing) for the model contracts and a full walkthrough.

## Config merging

When the package registers, it deep-merges its defaults *under* your published values using `array_replace_recursive`. This matters because Laravel's standard `mergeConfigFrom` only merges top-level keys: with shallow merging, publishing a slim config such as `'onboarding' => ['enabled' => true]` would silently drop the package's nested `onboarding.page` default and disable registration. With Packstub Tenancy, you can safely publish a trimmed config file containing only the keys you change — every nested default you omit is filled in.

One caveat: a deep merge combines by key, so it can never *shrink* an array default. If a future release ships a non-empty list-type default (for example, entries in `menu.items`), you could not remove those entries from the config file alone — you would override the whole array via the fluent API instead, which replaces values wholesale rather than merging. Today every list-type key (`menu.items`, `middleware.extra`, `resource_syncing.pairs`) defaults to `[]`, so this is a forward-looking constraint, not a current limitation.
