# Multi-tenancy: how a tenant is resolved and isolated

## 1. Resolution by domain (never by client input)

Every tenant request arrives on a **host**. The host is read from the server
context (`$request->getHost()`), which is the address the client dialed — it is
**never** taken from a body field, query string or header. A client therefore
cannot claim to be a tenant it does not own.

`DomainTenantResolver`:

1. Normalizes the host (`HostNormalizer`: lower-case, strip port, strip trailing
   dot, drop `www.`).
2. Looks up an **exact** match in `tenant_domains.host` (globally unique).
3. If no row matches (or the host is a WEBX platform host), the request is
   **refused** — there is no default/fallback tenant in production.

Domain states: `pending → verifying → active → failed → suspended`. Only
`active` domains serve traffic.

## 2. Server-side authorization gate

`TenantResolutionGuard` runs before any tenant connection is opened and asserts:

- the **domain** is `active` → else `403 domain_not_active`
- the **tenant** is `active` → else `403 tenant_not_active`
- the tenant has a **serviceable subscription** (configurable) → else
  `402 subscription_inactive`

Unknown host → `404 tenant_not_identified`.

## 3. Connection hydration

`TenantConnectionManager.configureFor($tenant)`:

- reads the tenant's `TenantDatabaseConnection` from the **central** DB,
- **decrypts** the password (stored with the `encrypted` cast),
- validates the driver against an **allow-list** (`mysql`, `mariadb`, `sqlite`)
  so an injected DSN can never select an arbitrary driver,
- writes a minimal, known-safe config into `database.connections.tenant`,
- **purges** the connection pool so no previous tenant's PDO handle is reused.

`forget()` restores the empty template and purges again, so nothing leaks
between requests, jobs or tests.

## 4. Private filesystem

`TenantFilesystemBootstrapper` repoints the private `tenant` disk to
`storage/app/tenants/{tenant_uuid}` for the current tenant. The disk has no
public URL; tenant files (passports, documents) are never linked from `public/`.

## 5. Queue jobs carry only a UUID

A tenant-scoped job uses the `TenantAware` trait. On construction it records the
**current tenant UUID** (never a live connection). Its job middleware
(`InitializeTenancyForJob`) re-fetches and re-validates that tenant from the
central catalog before the job runs, executes the body in that context, and
restores the previous context afterwards.

This guarantees a job built for tenant A can never run against tenant B's
connection left over on a worker — verified by `TenantJobIsolationTest`.

## 6. Console safety

- `core:migrate` / `tenant:migrate` show the destination database before any
  mutation and require confirmation (or `--force`).
- `tenant:migrate` requires an explicit tenant UUID or `--all`; there is no
  implicit bulk operation.
- `tenant:provision` names the tenant database from a technical id.

## What is NOT trusted

| Input | Trusted as tenant authority? |
|---|---|
| Server host (SNI/Host header at the edge) | ✅ yes — the resolution key |
| `tenant_id` / `tenant` in body or query | ❌ never |
| `X-Tenant-Id` or any client header | ❌ never |
| A job's serialized payload | Only the UUID, and it is re-validated |
