Le problème du multi-tenant naïf

La première approche qui vient à l'esprit : ajouter un champ tenant_id sur chaque entité et filtrer les requêtes manuellement. Ça fonctionne en développement. En production, c'est une bombe à retardement.

Un développeur qui oublie le filtre WHERE tenant_id = ? dans une seule requête expose les données d'un client à un autre. Le compilateur et les test unitaires n'avertissent pas.

Résolution de tenant via le domaine

Comment identifier le tenant d'une requête entrante ? La méthode la plus propre en SaaS : le sous-domaine. Chaque client a son propre sous-domaine (acme.myapp.io), que l'on résout vers un Tenant en base.

PHP src/Tenant/TenantResolver.php
final class TenantResolver
{
    public function __construct(
        private readonly TenantRepository $tenants,
        private readonly CacheInterface $cache,
    ) {}

    public function resolveFromRequest(Request $request): TenantId
    {
        $host = $request->getHost();   // ex: "acme.myapp.io"
        $slug = explode('.', $host)[0]; // "acme"

        return $this->cache->get(
            "tenant_id.$slug",
            fn() => $this->tenants->findBySlugOrFail($slug)->getId(),
        );
    }
}

On cache le résultat dans Redis avec une TTL courte. La résolution de tenant est sur le chemin critique de chaque requête.

Async isolé : Symfony Messenger et le contexte tenant

Le piège classique : un worker Messenger traite un message sans savoir à quel tenant il appartient. La solution est de sérialiser le tenant dans le message via un Stamp dédié.

PHP src/Messenger/TenantStamp.php
final readonly class TenantStamp implements StampInterface
{
    public function __construct(
        public readonly TenantId $tenantId,
    ) {}
}

// Middleware d'injection (côté dispatch)
final class TenantStampMiddleware implements MiddlewareInterface
{
    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        if ($envelope->last(TenantStamp::class) === null) {
            $envelope = $envelope->with(new TenantStamp($this->context->getTenantId()));
        }

        return $stack->next()->handle($envelope, $stack);
    }
}

Conclusion

L'isolation multi-tenant n'est pas une feature — c'est une propriété de sécurité fondamentale. Le résultat : une architecture où une fuite inter-tenant est structurellement impossible, pas juste improbable.