# Domains, routing, and Filament panels

## Domain records

The central `domains` table is represented by `TenantDomain` and contains:

- `tenant_id`
- normalized `domain`
- `type` (`subdomain` or `custom`)
- `is_primary`
- `verification_status`
- `verification_token_hash` (never exposed)
- `verified_at`
- `ssl_status`
- `last_checked_at`

`TenantDomainStatus` supports `pending`, `verified`, `active`, `failed`, and `suspended`.

## Platform subdomains

Tenant creation reserves the slug and creates a primary subdomain. The subdomain is marked active only after the tenant database passes provisioning smoke checks. The platform domain comes from `TENANCY_PLATFORM_DOMAIN` and is normalized to a host.

Reserved host prefixes are `www`, `console`, `admin`, `api`, `mail`, `support`, and `status`.

## Custom domains

Use `TenantDomainService::addCustomDomain()` instead of creating a domain row directly. It normalizes the host, rejects invalid/reserved/duplicate domains, and returns the one-time plaintext verification token while storing only its hash.

Verification options:

```bash
# Verify a token directly (useful for controlled tests).
php artisan tenant:domain:verify <domain-record-id> <token>

# Verify the TXT record at _matjari-verification.<domain>.
php artisan tenant:domain:verify <domain-record-id> <token> --dns

# Queue DNS verification with retries/backoff.
php artisan tenant:domain:verify <domain-record-id> <token> --dns --queue
```

The TXT record is:

```text
name:  _matjari-verification.example.com
type:  TXT
value: <token>
```

Only verified/active domains can become primary. `setPrimary()` locks the selected domain and all sibling domains in one central transaction, so there is never more than one primary domain per tenant.

SSL certificates and edge routing are managed by the hosting/CDN layer; the application records the status through `TenantDomainService::updateSslStatus()`.

## Request middleware

The store panel middleware order is significant:

1. `PreventAccessFromCentralDomains` rejects central hosts.
2. `InitializeTenancyByDomainOrSubdomain` resolves the tenant from the host.
3. `EnsureVerifiedTenantDomain` requires a verified/active matching domain.
4. `EnsureActiveTenant` rejects provisioning, pending-admin, suspended, failed, and archived stores.
5. `EnsureStoreFeatureForRequest` blocks disabled resource paths.
6. `UseStoreSessionCookie` selects the store-specific session cookie.
7. `ScopeSessions` prevents session reuse across tenant contexts.

The central `/admin` and `/console` panels use `EnsureCentralDomain`, which normalizes casing and trailing dots before comparison.

Unknown, unverified, suspended, or archived domains return 404 rather than revealing tenant existence.

## Filament panels

| Panel | URL | Guard/model | Purpose |
| --- | --- | --- | --- |
| Admin | `/admin` | `company_admin` / `CompanyAdmin` | Legacy central panel; business resources are compatibility-only when `LEGACY_CENTRAL_SCHEMA=true`. |
| Console | `/console` | `company_admin` / `CompanyAdmin` | Company tenant-management console. |
| Store | `/store` | `store_admin` / `Admin` | Tenant business resources. |

The Console panel contains tenant resources, domains/features/provisioning relation managers, lifecycle actions, Shield authorization, activity logging, and Matjari branding. It must not contain tenant business resources.

The Store panel contains tenant business resources and excludes central tenant-management resources. Feature-dependent resource navigation and relation-manager tabs are hidden when a feature is disabled, while request middleware still rejects direct access.

## Adding a new tenant resource

1. Put its migration under `database/migrations/tenant`.
2. Use a tenant-connected model or `TenantConnection` concern.
3. Register it in `StorePanelProvider`, not the Console panel.
4. Add a `StoreFeature` mapping if optional.
5. Add the route key to `EnsureStoreFeatureForRequest` if the feature is optional.
6. Use localized Filament labels, sections, one-column root schemas, and distinct icons.
7. Add tenant isolation and feature-disabled tests.

Never solve tenant isolation by adding a central `tenant_id` filter to a tenant-owned query.
