# Multi-Tenant Store Platform Plan

For implementation and operations guidance, see the [multi-tenant developer documentation](docs/multi-tenancy/README.md).

## Summary

Convert the current single-store Laravel 13 + Filament 5 application into a SaaS platform with:

- A central company console for managing stores, domains, provisioning, and features.
- A tenant store panel for each store’s operators.
- One database per tenant/store.
- Platform subdomains plus verified custom domains.
- Store-local users, admins, roles, permissions, catalogue, orders, geography, and configuration.
- Fresh-start deployment; existing YSF data will not be imported.

Use `stancl/tenancy` v3.10.x as the database-tenancy layer. Its current release supports Laravel 13, and it provides tenant database bootstrapping, domain identification, migrations, seeding, cache/filesystem isolation, and tenant lifecycle hooks ([package compatibility](https://packagist.org/packages/stancl/tenancy), [configuration](https://tenancyforlaravel.com/docs/v3/configuration/)).

Filament’s native tenancy features will not be used for data isolation. Database switching will be handled by Stancl Tenancy, while Filament will provide the two user interfaces. This avoids adding `tenant_id` columns to every business table. Filament’s domain and tenant security behavior should still guide panel routing and authorization ([Filament tenancy](https://filamentphp.com/docs/5.x/users/tenancy)).

## 1. Database architecture

### Central database

The central database stores only company-level control-plane data:

- `admins`
- central Spatie roles, permissions, and activity logs
- `tenants`
- `tenant_domains`
- `tenant_features`
- `tenant_provisioning_runs`
- central jobs, cache, and console sessions

Recommended `tenants` fields:

- `id` UUID/ULID
- `name` localized JSON (`ar`, `en`)
- `slug`
- `status`: provisioning, pending_admin, active, suspended, failed, archived
- `logo_path`
- `favicon_path`
- `theme_color`
- `default_locale`
- `database_name`
- `database_connection_key`
- `provisioned_at`
- `initial_store_admin_created_at`
- `suspended_at`
- `archived_at`
- `created_by_admin_id`
- timestamps

Database credentials must not be entered as ordinary form fields. Store only an encrypted connection reference or secret-manager key.

### Tenant database

Every tenant receives a separate database containing:

- `admins` table used by the `Admin` model
- tenant-local Spatie roles and permissions
- tenant-local activity logs
- users, customers, drivers, providers
- addresses and devices
- countries, cities, zones, currencies, exchange rates
- categories, brands, products, variants, images
- coupons, wallet, loyalty
- orders, payments, deliveries, ratings, evaluations
- notifications and content
- tenant configuration

The existing business migrations should be split into:

- central migrations
- tenant migrations

Tenant migrations must run only against tenant databases. Existing `created_by_admin_id` and `updated_by_admin_id` columns may remain for compatibility, but their relationships must resolve to the local tenant `Admin` model, never the central `CompanyAdmin` model.

Because the selected strategy is fresh start, production setup should reset and migrate the new structure instead of attempting to preserve the current single-database layout.

## 2. Models and services

Create central models:

- `Tenant`
- `TenantDomain`
- `TenantFeature`
- `TenantProvisioningRun`

Create central authentication model:

- `CompanyAdmin`

Create tenant authentication model:

- `Admin`

Keep the `Admin` model exclusively for tenant/store users.

Create services:

- `TenantProvisioningService`
- `TenantConnectionManager`
- `TenantDomainService`
- `TenantFeatureService`
- `TenantAdminProvisioningService`

Create a feature registry backed by a `StoreFeature` enum or validated feature-key registry. Initial features should include:

- catalogue
- orders
- delivery
- coupons
- wallet
- loyalty
- ratings
- notifications
- content
- advertising
- multi-currency

Feature checks must be enforced in:

- policies
- routes/middleware
- Filament resource visibility
- backend services and jobs

Hiding a navigation item alone is not sufficient.

## 3. Tenant provisioning workflow

Provisioning must be asynchronous and idempotent.

Flow:

1. Company admin creates a store in the console.
2. Store is created with status `provisioning`.
3. A unique database name is generated from the tenant UUID.
4. A queued provisioning job creates the database.
5. Tenant migrations are executed.
6. Tenant seeders create:
   - baseline roles and permissions
   - tenant-owned geography and currency catalogues
   - default delivery times and configuration
7. Smoke checks verify the tenant connection, required tables, and seed data.
8. Store status changes to `pending_admin`.
9. Company staff use the console's **Create Store Admin** action.
10. The action switches to the tenant database, creates the first `Admin`,
    assigns the local `super-admin` role, and records the action.
11. Store status changes to `active` and the default subdomain becomes available.

Required commands:

- `tenant:create`
- `tenant:provision`
- `tenant:migrate`
- `tenant:seed`
- `tenant:admin:create`
- `tenant:suspend`
- `tenant:activate`
- `tenant:archive`
- `tenant:database:destroy` (requires an archived tenant and `--confirm`)
- `tenant:domain:verify`

Failure handling:

- provisioning retries safely
- failed runs store error details
- partial databases are marked failed and cleaned up only through a protected action
- suspension disables login and domain routing without deleting data
- archive/deletion requires explicit confirmation and asynchronous database destruction

## 4. Filament panels

### Company console

Create a central Filament panel:

- panel ID: `console`
- route: `/console`
- central company domain
- authentication guard: `company_admin`
- model: `CompanyAdmin`

Console resources:

- `TenantResource`
- Tenant domains relation manager
- Tenant features relation manager
- Provisioning runs relation manager
- Create Store Admin action/page

The Create Store Admin action is available only when the tenant is in
`pending_admin`. It accepts name, username, email, temporary password,
password confirmation, and active status, then runs inside the tenant context,
assigns the local `super-admin` role, records
`initial_store_admin_created_at`, and moves the tenant to `active`.

Tenant actions:

- activate
- suspend
- retry provisioning
- create initial store admin
- verify domain
- set primary domain
- enable/disable features
- archive tenant

The existing Shield and activity-log integrations remain available for company administrators.

### Store panel

Create a tenant Filament panel:

- panel ID: `store`
- route: `/store` during the migration stage
- resolved only on tenant domains
- authentication guard: `store_admin`
- model: `Admin`

Register the current business resources in this panel:

- accounts
- catalogue
- geography
- configuration
- orders
- payments
- coupons
- wallet and loyalty
- notifications
- content

The store panel must not include central tenant-management resources.

Store operators belong to one tenant database and cannot switch tenants. Cross-tenant access is not allowed in v1.

Filament supports separate panels and domain-specific panel configuration ([panel configuration](https://filamentphp.com/docs/5.x/panel-configuration)).

## 5. Domain and subdomain routing

### Platform subdomains

At tenant creation:

- validate and reserve the slug
- create `{slug}.platform-domain`
- mark it active after provisioning succeeds

Reserved names must include:

- `www`
- `console`
- `admin`
- `api`
- `mail`
- `support`
- `status`

### Custom domains

`tenant_domains` should contain:

- `tenant_id`
- `domain`
- `type`: subdomain/custom
- `is_primary`
- `verification_status`
- `verification_token_hash`
- `verified_at`
- `ssl_status`
- `last_checked_at`
- timestamps

Custom-domain flow:

1. Company admin enters the domain.
2. The console generates DNS instructions.
3. The owner adds a TXT verification record.
4. A verification job checks DNS ownership.
5. The domain becomes active only after successful verification.
6. SSL certificate and edge routing are handled by the hosting/CDN layer.
7. The console records deployment and SSL status.

Use strict host validation. Unknown, unverified, suspended, or archived domains must return 404 or a tenant-unavailable response.

## 6. Tenant resolution and isolation

Add tenant identification middleware before store-panel requests:

- resolve the host against `tenant_domains`
- load the central `Tenant`
- initialize the tenant database connection
- reject inactive tenants
- persist tenant context through Livewire requests

Central models must always use the central connection. Tenant models must use the active tenant connection.

Every queued job must carry the tenant ID and initialize tenancy before accessing tenant data. Cache and storage keys must include tenant identity. Tenant file paths should use:

```text
tenants/{tenant-uuid}/...
```

Use Redis for tenant-aware cache tagging where possible; tenancy cache separation requires a tagging-capable cache store such as Redis ([Stancl configuration](https://tenancyforlaravel.com/docs/v3/configuration/)).

Production tenancy fails fast when the configured cache store is not taggable,
so a deployment cannot accidentally share cache entries between stores. Set
`CACHE_STORE=redis` in production.

Use separate session cookies for console and store panels.

The store panel uses the `TENANCY_STORE_SESSION_COOKIE` cookie name and
Stancl's `ScopeSessions` middleware to prevent both cross-panel and
cross-tenant session reuse.

## 7. Security and authorization

Implement:

- separate `company_admin` and `store_admin` guards
- separate Spatie permission sets per database
- central permissions for tenant lifecycle operations
- tenant permissions for store resources
- the initial `Admin` can be created only by an authorized company admin
- the temporary password is hashed immediately and is never stored centrally
- the first store admin must change the temporary password on first login
- fail-closed tenant resolution
- domain ownership verification
- encrypted database connection metadata
- no arbitrary connection values from HTTP requests
- tenant-aware `unique` and `exists` validation
- cross-tenant 404 behavior for guessed URLs
- tenant ID in logs, jobs, and audit context
- no cross-tenant impersonation in v1

## 8. Testing and acceptance criteria

Add tests for:

- tenant creation and provisioning state transitions
- idempotent provisioning and retry behavior
- tenant database migration and seeding
- two tenants cannot read or modify each other’s records
- central models never query tenant databases
- tenant models use the active tenant connection
- Admin authentication on the correct domain
- company Admin cannot log into the store panel
- unverified custom domains are rejected
- suspended tenants cannot authenticate
- subdomain and custom-domain resolution
- feature-disabled resources are hidden and return authorization failures
- tenant-specific cache, storage, queues, and sessions
- tenant context is restored inside queued jobs
- domain uniqueness and reserved slug validation
- console actions require Spatie permissions
- tenant database archival/deletion protections

Acceptance criteria:

- Creating a store provisions a working isolated database.
- Two stores can use identical IDs, emails, product codes, and category names without collisions.
- A tenant request never executes against another tenant’s database.
- Company staff can manage domains and features without direct tenant database editing.
- Store operators see only their own store data.
- Existing store resources continue to work under the tenant connection.
- No YSF data is imported in the initial rollout.

## Assumptions

- The application will use MySQL in production for tenant databases.
- The company owns and manages the central platform domain and DNS/CDN infrastructure.
- The first store admin is created manually by company staff; later store admins
  are managed from the tenant panel.
- Tenant-owned catalogues are independent after seeding; there is no live central geography synchronization in v1.
- Billing and subscription plans are deferred; feature access uses per-store flags.
- Public API work remains out of scope for this tenancy phase.

## Current implementation status

The first tenancy slice is implemented in the application:

- `stancl/tenancy` v3.10 is installed and registered.
- Central `Tenant`, `TenantDomain`, `TenantFeature`, and
  `TenantProvisioningRun` models/migrations are available.
- Tenant databases are created, migrated, seeded, and tracked through
  `TenantProvisioningService` and `ProvisionTenantJob`.
- `Admin` uses the tenant connection and has its own `store_admin` guard.
- Console and store Filament panels are registered with domain-aware tenant
  middleware; central tenant-management resources are excluded from the store
  panel. The store panel currently uses `/store` while the legacy central
  `/admin` panel remains for backward compatibility; it can move to `/admin`
  once that legacy panel is retired.
- Domain token/TXT verification, primary-domain selection, feature flags,
  lifecycle commands, and an active-tenant guard are implemented.
- The console domain relation manager now creates custom domains through the
  verification service, displays TXT instructions, supports token verification,
  and protects the primary domain from deletion.
- DNS ownership checks can run through the retryable
  `VerifyTenantDomainJob` using `tenant:domain:verify --dns --queue`.
- Store-feature flags now hide the matching store-panel navigation resources
  and reject direct resource requests through tenant middleware. Queued tenant
  database destruction re-checks the archived state immediately before
  deleting, preventing a stale destructive job from removing a reactivated
  store database.
- Feature-dependent relation managers are also hidden when their store feature
  is disabled (for example, deliveries, ratings, wallet, loyalty, coupons,
  notifications, and payment-method availability).
- Multi-currency now gates both currency and exchange-rate resources, including
  direct store requests.
- Relation-manager forms use translated sections and an explicit one-column
  root layout, and all relation tabs expose localized titles and labels.
- Tenant requests use a dedicated store session cookie and `ScopeSessions`,
  preventing session collisions between the central and store panels.
- Focused tenancy tests and the complete existing Pest suite pass.
- End-to-end coverage now provisions a SQLite tenant database, verifies the
  baseline schema and seed data, creates the first tenant `Admin`, and
  confirms its local `super-admin` role and hashed temporary password.
- End-to-end coverage also verifies two provisioned tenants can use identical
  user IDs and email addresses without reading each other's records, and that
  tenant context is restored to the central connection after scoped work.
- The singular `tenant:migrate` and `tenant:seed` commands are available as
  safe one-tenant wrappers around Stancl's migration and seeding commands.
- Provisioning claims runs under a central row lock, so direct retries cannot
  create duplicate active runs; primary-domain changes are transactionally
  serialized as well. Suspend, activate, and archive transitions use the same
  central locking boundary.

Central migrations now live in `database/migrations/central`, while tenant
business, user, authorization, activity-log, and `Admin` migrations live
in `database/migrations/tenant`. The normal `migrate` command runs only the
central path; `tenant:migrate` runs the tenant path against the selected store
database. Set `LEGACY_CENTRAL_SCHEMA=false` in production. The test suite
temporarily enables the legacy compatibility flag; to build that schema
locally, pass `--path=database/migrations/tenant` explicitly.

Seeders follow the same boundary. `DatabaseSeeder` delegates to
`CentralDatabaseSeeder` and seeds only central authorization and company-admin
records. `TenantDatabaseSeeder` is selected by `tenant:seed` and tenant
provisioning; it seeds tenant defaults, authorization, geography, and delivery
times on the tenant connection.
