# Tenancy internals

## Connections

| Name | Who uses it | Privileges |
|---|---|---|
| `master` | Web requests, workers | DML on `social_saas_master` only |
| `master_migrator` | `composer migrate:master` at deploy time | DDL on master |
| `provisioner` | Only the provisioning worker (`ProvisionTenant` job, `tenants:provision`, `tenants:rotate-credentials`) | `CREATE`/`GRANT` on `social_tenant_%`, `CREATE USER` |
| `tenant` | Anything touching a tenant model, after `TenantManager::initialize()` | Its own schema only (one MySQL user per tenant) |

The `tenant` connection is a template with no credentials. `TenantConnectionConfigurator` fills it in from `tenant_database_connections` plus the `SecretStore`, and `end()` restores the empty template and purges the PDO.

Tenant models use `App\Models\Concerns\UsesTenantConnection`. Touching one with no tenant initialised throws `NoTenantInitialized` instead of connecting anywhere. An architecture test enforces that every model declares its connection trait.

## Lifecycle of a request

1. `AssignRequestId` (global) adds `request_id` to log context and the response header.
2. `EndTenancy` (web group) calls `TenantManager::end()` when every request terminates.
3. One host serves everything. There are no subdomains or custom domains; the URL never names the workspace.
4. Workspace route group: `auth → verified → tenant → tenant.active → tenant.member → tenant.init → mfa`.
   - `tenant` (`IdentifyTenant`) reads the current workspace from the session (`WorkspaceSession`, key `tenancy.current_tenant_id`). The choice is honoured only while the user has an active membership there, re-checked on every request. With no valid choice, a user with exactly one workspace is placed in it; anyone else is redirected to `/workspaces`.
   - The workspace is chosen at `/workspaces/{slug}/enter` (picker, top-bar switcher, and links in e-mails via `TenantUrl::to($tenant, $path)`, which adds `?next=/path`; only same-site paths are followed).
   - `tenant:optional` (used by `/security`) opens the current workspace if there is one, so account pages keep its layout, and never redirects.
   - `IdentifyTenant` and `EnsureTenantActive` sit right after `auth` in Laravel's middleware priority list, before route-model binding.
5. Livewire update requests re-run the same middleware through `Livewire::addPersistentMiddleware()`.
6. Because the choice is per session, switching workspace in one tab affects every tab. `BindComponentToTenant` stores the workspace id in each component's checksummed snapshot memo and answers an update from another workspace with 419, which Livewire turns into a "reload the page" prompt. Media view URLs are signed with the workspace id and refused while a different workspace is current.

## Lifecycle of a queued job

```php
class SyncSomething implements ShouldQueue, TenantAware
{
    use InteractsWithTenant, Queueable;   // adds $tenantId + InitializeTenancy job middleware

    public function __construct(string $tenantId, public string $accountId)
    {
        $this->tenantId = $tenantId;
    }
}
```

- `InitializeTenancy` re-reads the tenant from master on every attempt, drops jobs for deleted tenants, refuses non-operational tenants, and ends tenancy in `finally`.
- `JobProcessing` resets any context left over from a previous job and logs an error. `JobProcessed`/`JobFailed`/`JobExceptionOccurred` always end tenancy.
- Never pass tenant-connection models or secrets in a job payload. Pass IDs.

## What `initialize()` scopes

| Resource | Scoped to |
|---|---|
| DB | `tenant` connection → tenant schema + tenant MySQL user |
| Cache | `cache.prefix` becomes `{base}t_{tenant_id}_` |
| Storage | `Storage::disk('tenant')` rooted at `tenants/{tenant_id}` on `filesystems.tenant_base_disk` |
| Logs | `Context` key `tenant_id` |

## Authorization

- Agency role: master `tenant_users.role`. Permissions: `config/rbac.php`, exposed as Gate abilities (`Gate::before`) evaluated against the **current tenant membership** only.
- Role changes go through `ChangeMemberRole` (no self-change, owner is immutable, an actor can only assign roles listed in `rbac.assignable_by`).
- Super admins have no implicit tenant access (`support_access_grants` is modelled and will be wired up with the support tooling).
- Client-level scoping (`client_users`) arrives in M2.

## Provisioning

See [runbooks/provisioning.md](../runbooks/provisioning.md). Steps are listed in `config/tenancy.php` and each is idempotent. Progress lives in `tenant_provisioning_jobs`, one row per step.
