# ADR-003 : Stratégie multi-tenant (résolution par singleton applicatif + global scope)

**Statut :** Accepté
**Date :** 2026-07-07
**Contexte :** Isolation des données entre companies (tenants) dans toute l'application

---

## Contexte

gestionstock est un SaaS multi-tenant : chaque `Company` doit voir exclusivement ses propres données (produits, commandes, utilisateurs, etc.). Ce mécanisme est central et déjà en place depuis l'introduction du multi-tenant (migration `2026_02_14_000001_add_saas_multi_tenancy.php`), mais n'était documenté nulle part — aucun ADR WES existant ne couvre la stratégie multi-tenant. Un audit a signalé ce manque de documentation pour un mécanisme aussi structurant.

## Décision

La résolution du tenant courant repose sur trois pièces coordonnées :

1. **`App\Http\Middleware\ResolveTenant`** (middleware global, `app/Http/Kernel.php`) : résout la company à partir de l'utilisateur JWT authentifié (`auth('api')->user()->company_id`), avec repli sur la company du contexte superadmin actif (session), puis sur la « première company active » pour les routes sans authentification (ex: `/app`, `/all-langs`, pages publiques du frontend). Le résultat est lié au conteneur via `app()->instance('current_company', $company)`.
2. **`App\Traits\BelongsToCompany`** : appliqué sur tous les modèles tenant-scopés. Assigne automatiquement `company_id` à la création (`if (empty($model->company_id))`) et enregistre le global scope `CompanyScope`.
3. **`App\Scopes\CompanyScope`** : filtre toute requête Eloquent sur les modèles concernés par `company_id = current_company.id`, uniquement si `app()->bound('current_company')` — ne s'applique donc pas si le tenant n'a pas pu être résolu (routes superadmin `withoutGlobalScopes()`, etc.).

Le helper `company()` (`app/Classes/start.php`) offre une résolution de secours (singleton > session > utilisateur connecté > première company) pour le code qui a besoin du tenant courant en dehors du cycle de requête HTTP standard (jobs, commandes artisan).

## Alternatives Considérées

### Colonne `company_id` filtrée explicitement à chaque requête (pas de global scope)
Rejetée : oblige chaque développeur à se souvenir d'ajouter le filtre à chaque requête sur un modèle tenant-scopé — source d'erreur systémique (fuite cross-tenant par oubli). Le global scope rend la sécurité par défaut.

### Base de données séparée par tenant
Rejetée : complexité opérationnelle disproportionnée pour la taille actuelle de la plateforme (migrations, sauvegardes, connexions à gérer par tenant) ; le volume de données par tenant ne le justifie pas.

## Conséquences

- Toute nouvelle table portant des données propres à un tenant doit utiliser `BelongsToCompany` — un oubli réintroduit une classe de vulnérabilité déjà corrigée une fois dans ce projet (guards mass-assignment sur ~28 modèles, audit 2026-07).
- Le fallback « première company active » de `ResolveTenant` pour les routes non authentifiées est une source de risque connue : toute nouvelle route publique doit être examinée pour vérifier qu'elle ne fuit pas de données via ce mécanisme (cf. `PublicRouteExposureTest.php`).
- `app()->instance('current_company', ...)` est une exception assumée à la règle générale WES « ne jamais utiliser `app()`/`resolve()` dans la logique de domaine » — le tenant courant est un contexte transverse à toute la requête, pas une dépendance métier classique injectable proprement via le constructeur sans réécrire l'ensemble des Models/Controllers.

## Quand Réviser

- Si un modèle tenant-scopé est ajouté sans `BelongsToCompany` (à détecter idéalement via un Arch Test).
- Si le volume de tenants ou la sensibilité réglementaire des données justifie un jour une isolation plus forte (base séparée, chiffrement par tenant).
- Si une nouvelle route publique est envisagée : vérifier explicitement son comportement vis-à-vis du fallback de `ResolveTenant`.
