Un éditeur d’extension de réservation pour salons de coiffure nous a demandé, il y a quelques mois, de reprendre son extension freemium qui commençait à devenir ingérable : la version pro était un fork complet de la version gratuite, avec des correctifs appliqués parfois d’un côté, parfois de l’autre, jamais des deux. Résultat, des bugs corrigés en gratuit ressurgissaient en pro six mois plus tard. C’est l’antipattern classique du freemium mal architecturé : deux bases de code au lieu d’une base et une surcouche.
Une architecture freemium saine repose sur un principe simple : la version gratuite est un produit complet et autonome, la version pro est une extension de cette version gratuite, jamais une copie modifiée. Cela suppose de concevoir dès le départ des points d’extension clairs, plutôt que de laisser le pro réécrire des bouts du gratuit.
Deux extensions, une seule vérité
Concrètement, on structure le projet en deux plugins WordPress distincts : mon-extension (gratuit, publié sur WordPress.org) et mon-extension-pro (payant, distribué en dehors). Le plugin pro déclare une dépendance logique envers le gratuit — pas forcément via Requires Plugins si l’on veut rester compatible avec des versions antérieures à WordPress 6.5, mais au minimum via une vérification au chargement :
add_action( 'plugins_loaded', function() {
if ( ! class_exists( 'Mon_Extension\Core' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="notice notice-error"><p>'
. esc_html__( 'Mon Extension Pro nécessite Mon Extension (version gratuite) activée.', 'mon-extension-pro' )
. '</p></div>';
} );
return;
}
Mon_Extension_Pro\Bootstrap::init();
}, 20 );
La priorité 20, plus tardive que celle du gratuit (souvent 10 ou l’appel direct), garantit que le socle gratuit est déjà initialisé quand le pro tente de s’y accrocher.
Concevoir des points d’extension nommés

Le socle gratuit doit exposer des hooks explicites à chaque endroit où une fonctionnalité pro viendra se greffer, plutôt que de laisser le pro modifier des fichiers du gratuit ou dupliquer une méthode entière pour n’en changer qu’une ligne. Quelques exemples concrets pour une extension de réservation :
do_action( 'mon_extension_apres_creation_reservation', $reservation_id, $donnees )— le module pro de rappel SMS s’y accroche sans toucher au code de création.apply_filters( 'mon_extension_creneaux_disponibles', $creneaux, $prestation_id )— le module pro de gestion multi-praticiens filtre la liste sans réécrire le calcul de base.apply_filters( 'mon_extension_champs_reservation', $champs )— un module pro de champs personnalisés ajoute des entrées au tableau existant.
Cette liste de points d’extension devient, de fait, la documentation de l’API interne du gratuit. Elle doit être stable : renommer un hook entre deux versions casse tous les modules pro qui en dépendent, y compris ceux d’éventuels partenaires tiers si votre écosystème s’ouvre un jour à des extensions de modules complémentaires.
Éviter la tentation du if ( defined pro )
Un piège fréquent consiste à parsemer le code gratuit de conditions du type if ( defined( 'MON_EXTENSION_PRO' ) ) { ... }. Cela recrée exactement le couplage qu’on cherche à éviter : le gratuit devient conscient du pro, les deux bases redeviennent interdépendantes de façon fragile. La bonne pratique est unidirectionnelle : le pro connaît le gratuit et s’y accroche, jamais l’inverse.
Vérification de licence : où et comment
La vérification de licence doit vivre entièrement dans le plugin pro, jamais dans le socle gratuit. Une architecture courante et éprouvée :
- À l’activation du pro, un écran de réglages demande la clé de licence.
- Une requête HTTP (via
wp_remote_post()) vers votre serveur de licences valide la clé, le domaine et le nombre d’activations. - Le résultat (valide, expirée, invalide) est stocké dans un
transientavec une durée de vie courte (typiquement 12 à 24 heures), pour limiter les appels réseau sans bloquer le site en cas d’indisponibilité temporaire du serveur. - Une tâche planifiée via
wp_schedule_event()revalide périodiquement la licence en arrière-plan.
Le point le plus important, souvent négligé : que faire quand la licence expire ou devient invalide ? La réponse éthique et la plus courante chez les éditeurs sérieux est de continuer à faire fonctionner les fonctionnalités déjà configurées, mais de bloquer les mises à jour et le support. Couper brutalement une fonctionnalité active sur un site en production (par exemple arrêter d’envoyer les SMS de rappel déjà programmés) génère de la défiance et des demandes de remboursement, sans réel gain commercial.
Sur ce projet de réservation, on a tranché ainsi : licence expirée = plus de mise à jour et un bandeau discret dans l’admin, mais les réservations et rappels déjà configurés continuent de tourner. Le taux de renouvellement a été meilleur qu’avec l’ancienne version qui bloquait tout au bout de 30 jours.
Organiser le dépôt et le build
Deux dépôts séparés (un public pour le gratuit, un privé pour le pro) évitent qu’un extrait de code pro se retrouve par erreur dans le paquet publié sur WordPress.org — un incident qui arrive plus souvent qu’on ne le pense avec un dépôt unique et des branches mal isolées. Un script de build assemble ensuite le paquet pro final en copiant le gratuit comme dépendance de développement, ou en s’appuyant sur Composer avec un dépôt privé référencé dans composer.json.
Notre verdict
Une architecture freemium qui vieillit bien repose sur trois piliers : un socle gratuit qui reste un produit complet, des points d’extension nommés et stables plutôt qu’un couplage bidirectionnel, et une gestion de licence qui dégrade en douceur plutôt que de couper brutalement. Le coût initial est un peu plus élevé — il faut réellement concevoir l’API interne — mais il évite la dérive du double maintien de code que subissait notre client, et il facilite l’ouverture future à des modules tiers si l’écosystème grandit.