# Architecture and database boundaries

## Topology

Matjari uses database-per-tenant tenancy through `stancl/tenancy`. One central database stores platform control-plane records. Each store receives an isolated database containing its own operators and business data.

```text
Central database
├── company_admins                 company users
├── roles / permissions / activity_log
├── tenants                        store registry and lifecycle
├── domains                        platform and custom domains
├── tenant_features                feature flags
├── tenant_provisioning_runs       provisioning attempts
└── jobs / cache / console sessions

Tenant database (one database per store)
├── admins                         Admin records
├── roles / permissions / activity_log
├── users, devices, addresses
├── geography, currencies, delivery times
├── catalogue, favourites, providers
├── orders, payments, delivery, ratings
├── coupons, wallet, loyalty
├── notifications, content, configuration
└── tenant-local session/queue data as configured
```

The complete migration split is physical:

- `database/migrations/central`: central cache/jobs, tenants/domains, company admins, central authorization/activity log, feature flags, and provisioning runs.
- `database/migrations/tenant`: users, all business tables, tenant authorization/activity log, and `Admin`.

`AppServiceProvider` registers only the central migration path for normal `migrate` commands. The tenant path is added automatically only for the testing compatibility suite. `config/tenancy.php` points Stancl's `tenants:migrate` command at the tenant path. To intentionally build the legacy compatibility schema locally, pass `--path=database/migrations/tenant` explicitly.

## Legacy central compatibility

`LEGACY_CENTRAL_SCHEMA` exists only for migration-stage local/testing compatibility:

| Environment | Default | Purpose |
| --- | --- | --- |
| `local` | `true` | Keeps the old `/admin` business resources usable while migrating. |
| `testing` | `true` in `phpunit.xml` | Lets legacy and tenancy tests share the test database. |
| `production` | `false` | Prevents business tables from being created centrally. |

Set `LEGACY_CENTRAL_SCHEMA=false` explicitly in production. When it is false, the legacy `/admin` panel registers only `CompanyAdminResource` and the Shield role resource; business resources belong to `/store`.

## Models and connections

### Central models

`Tenant`, `TenantDomain`, `TenantFeature`, `TenantProvisioningRun`, and `CompanyAdmin` use the central connection. `Tenant` extends Stancl's tenant model and implements `TenantWithDatabase`; its lifecycle columns are real, indexed columns rather than arbitrary `data` metadata. `CompanyAdmin` is the company-console authenticatable model.

### Tenant models

Business models and `Admin` use Stancl's `TenantConnection` concern. The tenant connection is dynamically configured when tenancy is initialized. Do not add a `tenant_id` column to tenant-owned tables.

The `created_by_admin_id` and `updated_by_admin_id` compatibility columns on tenant records must point to local `Admin` records when a relationship is used. They must never query the central `company_admins` table from a tenant connection.

### Scoped access

Use `TenantConnectionManager` for service code that needs a temporary tenant context:

```php
$value = app(TenantConnectionManager::class)->run($tenant, function (): mixed {
    return User::query()->where('email', $email)->first();
});
```

The manager restores the previous tenant (or the central context) in a `finally` block. Long-running workers must not retain a tenant context between jobs.

Never do this in a central request:

```php
User::query()->where('tenant_id', $tenantId)->get();
```

Instead, resolve the central `Tenant` first and run the query inside that tenant's initialized connection.

## Authentication boundaries

`config/auth.php` defines three guards:

| Guard | Model | Panel | Database |
| --- | --- | --- | --- |
| `company_admin` | `CompanyAdmin` | `/admin`, `/console` | central |
| `store_admin` | `Admin` | `/store` | active tenant |
| `web` | `User` | reserved for application clients | active/default context |

The first store admin is created only after provisioning reaches `pending_admin`. A temporary password is hashed in the tenant database, `force_password_change` is set, and the local `super-admin` role is assigned.

## Feature registry

`App\Enums\StoreFeature` is the only supported feature-key registry:

`catalogue`, `orders`, `delivery`, `coupons`, `wallet`, `loyalty`, `ratings`, `notifications`, `content`, `advertising`, and `multi_currency`.

`TenantFeatureService::initializeDefaults()` enables catalogue, orders, and delivery by default and disables the remaining features. Unknown feature strings return false in reads and throw during writes.

Feature enforcement currently exists at three UI/request boundaries:

1. Store resource navigation (`HasTenantFeature`).
2. Feature-dependent relation-manager tabs (`HasTenantFeatureRelationManager`).
3. Store request middleware (`EnsureStoreFeatureForRequest` and `EnsureTenantFeatureEnabled`).

Any future controller, job, command, or API must call `TenantFeatureService` or an equivalent policy check as well; hiding a navigation item is not authorization.
