# Antipatterns d’une extension multi-tenant sur un parc de 500 sites

> Sur un grand réseau de collectivités, les erreurs d'isolation entre sites ne se voient pas tout de suite : elles apparaissent le jour où un site hérite silencieusement des données d'un autre.

- Auteur : Clément Hadrot
- Publié le : 2026-09-08
- Mis à jour le : 2026-09-08
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/antipatterns-extension-multi-tenant-500-sites/

## L’essentiel

- Une option globale qui devait être locale à un site
- Des tables sans préfixe de site partagées par erreur
- Un identifiant de session qui traverse plusieurs sites

Une donnée qui apparaît sur le site de la mairie B alors qu'elle a été saisie sur le site de la mairie A : c'est le symptôme qui remonte en général bien après le déploiement initial d'une extension multi-tenant, sur un réseau de collectivités qui a grossi progressivement de quelques sites pilotes à plusieurs centaines d'installations. Le code fonctionnait très bien tant que le parc restait petit ; l'erreur d'isolation était simplement invisible avec peu de sites actifs en même temps.

Cet article recense les erreurs récurrentes observées sur ce type de parc. Il ne détaille pas l'architecture recommandée pour construire une extension multi-tenant saine dès le départ, déjà traitée dans un article dédié : il se concentre sur ce qu'on voit trop souvent en audit, pourquoi c'est un problème, et comment corriger.

## Antipattern : une option globale qui devait être locale

Ce qu'on voit : un réglage enregistré via `update_option()` sans passer par les fonctions propres au multisite, dans une extension censée fonctionner par site indépendant sur un réseau WordPress multisite.

Pourquoi c'est un problème : sur un multisite standard, `update_option()` écrit bien dans la table d'options propre au site courant grâce au préfixe de table dynamique — mais dès qu'une extension mémorise une valeur en cache d'objet sans clé de site explicite, ou qu'elle utilise `update_network_option()` par erreur de copier-coller pensant utiliser la fonction locale, le réglage devient partagé entre tous les sites du réseau sans que personne ne s'en aperçoive avant qu'un administrateur d'un site modifie un paramètre censé n'affecter que le sien.

```
// À éviter dans une extension censée rester locale à un site :
update_network_option( null, 'assoc_mode_maintenance', true );

// Ce qu'il faut faire :
update_option( 'assoc_mode_maintenance', true );
```

## Antipattern : des tables personnalisées sans préfixe de site

> L'essentiel à retenir : Une option globale qui devait être locale à un site ; Des tables sans préfixe de site partagées par erreur ; Un identifiant de session qui traverse plusieurs sites

Ce qu'on voit : une extension qui crée sa propre table via `dbDelta()` avec un nom fixe, comme `wp_assoc_signalements`, au lieu d'utiliser le préfixe dynamique fourni par `$wpdb->prefix` qui varie selon le site sur un réseau multisite.

Pourquoi c'est un problème : sur un réseau multisite, chaque site dispose normalement de son propre préfixe de table (`wp_2_`, `wp_3_`, etc.). Une table nommée en dur, sans respecter ce préfixage, devient une table réseau de fait, lue et écrite par tous les sites qui exécutent l'extension, quelle que soit leur intention initiale de rester isolés.

```
global $wpdb;
$table = $wpdb->prefix . 'assoc_signalements'; // respecte le site courant
$wpdb->query( "CREATE TABLE IF NOT EXISTS {$table} ( ... )" );
```

## Antipattern : un identifiant de session qui traverse les sites

Ce qu'on voit : un jeton de session utilisateur généré côté extension et stocké dans un cookie sans domaine restreint au site courant, ou dans une variable de cache partagée par clé trop générique du type `session_utilisateur`.

Pourquoi c'est un problème : sur un réseau multisite avec des sous-domaines proches (deux mairies voisines partageant le même domaine racine, par exemple), un cookie mal scoppé peut rester valide sur un autre site du réseau, ce qui permet à une session ouverte sur le site A de rester active par erreur sur le site B. La clé de cache doit systématiquement inclure l'identifiant du site courant, obtenu via `get_current_blog_id()`.

```
$cle_cache = 'session_utilisateur_' . get_current_blog_id() . '_' . $utilisateur_id;
wp_cache_set( $cle_cache, $donnees_session, 'assoc-sessions', 3600 );
```

## Antipattern : un cache d'objet non isolé par site

Ce qu'on voit : un groupe de cache non persistant déclaré sans tenir compte du contexte multisite, ce qui, selon la configuration du serveur d'objets partagé entre sites, peut mélanger des valeurs mises en cache par des sites différents si le groupe de cache n'est pas explicitement préfixé.

Pourquoi c'est un problème : un objet de cache mal isolé provoque des bugs intermittents, difficiles à reproduire, puisqu'ils dépendent de l'ordre dans lequel les sites écrivent et lisent le cache. Ce type d'erreur passe souvent inaperçu en test, où un seul site est actif à la fois, et n'apparaît qu'en charge réelle sur le parc complet.

## Comment auditer un parc existant

- Lister toutes les tables créées par l'extension et vérifier qu'aucune n'omet le préfixe dynamique de site.
- Rechercher dans le code toute occurrence de `update_network_option`, `get_network_option` ou `get_sites()` utilisée sans justification métier explicite de portée réseau.
- Vérifier que chaque clé de cache d'objet inclut l'identifiant de site courant, en particulier pour les données sensibles ou personnelles.

> Sur un réseau de cinq cents sites, une erreur d'isolation qui ne touche qu'un site sur mille reste statistiquement certaine d'arriver un jour. Ce n'est pas une question de probabilité faible, c'est une question de volume.

## En résumé

Les antipatterns d'isolation multi-tenant partagent un point commun : ils sont invisibles à petite échelle et deviennent des incidents de confidentialité à grande échelle. Vérifier systématiquement le préfixage des tables, la portée réelle de chaque option et le scope des clés de cache reste le meilleur usage du temps d'audit sur un parc de cette taille.
