WordPress y webs multilingües
Sistema de licencias para plugins de WordPress: cómo diseñar el servidor
Vender un plugin implica algo más que cobrar: activar sitios, validar claves y entregar actualizaciones. Arquitectura, riesgos y decisiones de un servidor de licencias.
Por AYM FlowPublicado: Actualizado: 8 min de lectura
Quien desarrolla un plugin de pago pronto descubre que el código es la parte fácil. Lo difícil es todo lo que rodea la venta: emitir claves, limitar los sitios donde se usan, entregar actualizaciones solo a clientes con licencia vigente y gestionar renovaciones sin romper ninguna web. Un sistema de licencias para plugins de WordPress resuelve eso. Aquí se explican su arquitectura, los puntos críticos de seguridad y los errores que conviene evitar.
Qué hace un sistema de licencias para plugins de WordPress (y qué no)
Un detalle previo condiciona todo el diseño: los plugins de WordPress que se distribuyen deben ser compatibles con la licencia GPL, lo que significa que el código no puede ocultarse ni bloquearse legalmente. Por eso, el sistema de licencias no «protege» el código; controla el acceso a lo que sí tiene valor comercial: actualizaciones, soporte, servicios en la nube y funciones asociadas.
En la práctica, un sistema de licencias debe:
- Emitir claves vinculadas a un cliente, un plan y una fecha de vencimiento.
- Registrar en qué sitios está activada cada clave y respetar el límite de cada plan.
- Validar periódicamente que la licencia sigue vigente.
- Autorizar la descarga de actualizaciones solo a licencias válidas.
- Permitir al cliente desactivar un sitio y trasladar la activación a otro.
Arquitectura: cliente en el plugin y servidor central
El diseño habitual tiene dos piezas. El cliente es un módulo dentro del plugin que muestra la pantalla de licencia, guarda la clave y habla con el servidor. El servidor es una aplicación aparte, con su propia base de datos, que expone una API pequeña y estable:
- Activar: recibe la clave, el dominio y la versión del plugin; comprueba el plan y el límite de sitios y registra la activación.
- Verificar: confirma el estado (activa, caducada, revocada) de forma periódica.
- Desactivar: libera una activación.
- Información de actualización: devuelve la última versión, el registro de cambios y una URL de descarga temporal para licencias válidas.
El servidor puede ser un plugin de WordPress en un sitio propio, un servicio de terceros o una aplicación a medida. Una aplicación propia ofrece más control sobre planes, reglas y facturación; es la ruta que AYM Flow siguió para AYM Multilingual, con licencias de uno a cinco sitios descritas en la página de precios.
Ejemplo: una comprobación de licencia que nunca rompe la web
El cliente dentro del plugin debe ser tolerante. Esta función consulta al servidor de licencias como mucho cada 12 horas, guarda el resultado en un transient y trata un error de red o una respuesta distinta de 200 como una caída, no como un veredicto. Así un problema del servidor nunca deja fuera a un cliente que ha pagado.
<?php
defined( 'ABSPATH' ) || exit;
/**
* True while the licence is active. Asks the server at most every 12 hours;
* an outage keeps the last known verdict instead of locking the customer out.
*/
function acme_license_is_active(): bool {
$cached = get_transient( 'acme_license_status' );
if ( false !== $cached ) {
return 'active' === $cached;
}
$response = wp_remote_post( 'https://license.example.com/wp-json/acme-license/v1/check', array(
'timeout' => 8,
'body' => array(
'license_key' => get_option( 'acme_license_key', '' ),
'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
'version' => ACME_VERSION, // defined in the main plugin file
),
) );
// Network error or non-200 = outage, not a verdict: retry in an hour.
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
$last = get_option( 'acme_license_last_status', '' );
set_transient( 'acme_license_status', $last, HOUR_IN_SECONDS );
return 'active' === $last;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$status = isset( $data['status'] ) ? sanitize_key( $data['status'] ) : 'invalid';
update_option( 'acme_license_last_status', $status, false );
set_transient( 'acme_license_status', $status, 12 * HOUR_IN_SECONDS );
return 'active' === $status;
}Los campos de la petición (license_key, domain, version) y la regla de que una caída devuelve un estado distinto de 200 reflejan el contrato de nuestro propio servidor de licencias. Define ACME_VERSION en el archivo principal del plugin, guarda la clave con update_option cuando el cliente la active y condiciona las funciones premium a acme_license_is_active().
Modelo de datos y reglas de negocio
Un modelo mínimo con tres tablas cubre casi todos los casos: licencias (clave, cliente, plan, estado, vencimiento), activaciones (licencia, dominio normalizado, versión, fecha de la última comprobación) y planes (límite de sitios, funciones incluidas). Algunas decisiones merecen atención:
- Normalizar el dominio (sin protocolo, sin `www`, en minúsculas) para no contar dos veces el mismo sitio.
- Distinguir entornos de desarrollo y producción, para que un sitio de pruebas local no consuma una activación.
- Definir el periodo de gracia tras el vencimiento: qué pasa con las actualizaciones y con las funciones.
- Registrar los eventos (activación, revocación, reembolso) para atender al soporte con datos, no con suposiciones.
Entrega de actualizaciones desde el servidor propio
WordPress comprueba las actualizaciones consultando un transient del sistema. Para que un plugin privado aparezca en la pantalla de actualizaciones, el cliente se engancha a ese proceso con el filtro `pre_set_site_transient_update_plugins` e informa de la nueva versión y de la URL de descarga. El filtro `plugins_api` permite además mostrar la ficha con el registro de cambios.
La URL de descarga no debe ser un enlace permanente: debe generarse al vuelo, estar firmada o asociada a un token con caducidad corta y comprobar la licencia en cada petición. De lo contrario, un enlace filtrado equivale a una licencia gratuita para siempre.
Ejemplo: ofrecer actualizaciones solo a sitios con licencia
WordPress consulta las actualizaciones de plugins a través del transient de sitio update_plugins. Con el filtro pre_set_site_transient_update_plugins un plugin premium puede añadir su propia versión a esa lista, pero solo mientras la licencia esté activa.
<?php
// Must live in the main plugin file: plugin_basename( __FILE__ ) has to match the plugin path.
add_filter( 'pre_set_site_transient_update_plugins', function ( $transient ) {
if ( empty( $transient->checked ) || ! acme_license_is_active() ) {
return $transient;
}
$plugin = plugin_basename( __FILE__ );
$remote = get_transient( 'acme_update_info' );
if ( false === $remote ) {
// Your endpoint returns {"new_version": "...", "package": "...", "url": "..."}.
$response = wp_remote_post( 'https://license.example.com/wp-json/acme-license/v1/update', array(
'timeout' => 8,
'body' => array(
'license_key' => get_option( 'acme_license_key', '' ),
'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
),
) );
$remote = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
$remote = is_array( $remote ) ? $remote : array();
set_transient( 'acme_update_info', $remote, 6 * HOUR_IN_SECONDS );
}
$installed = $transient->checked[ $plugin ] ?? '0';
if ( ! empty( $remote['new_version'] ) && ! empty( $remote['package'] )
&& version_compare( $remote['new_version'], $installed, '>' ) ) {
$transient->response[ $plugin ] = (object) array(
'slug' => dirname( $plugin ),
'plugin' => $plugin,
'new_version' => sanitize_text_field( $remote['new_version'] ),
'package' => esc_url_raw( $remote['package'] ), // short-lived, signed URL
'url' => esc_url_raw( $remote['url'] ?? '' ),
);
}
return $transient;
} );Este código debe estar en el archivo principal del plugin, porque plugin_basename(__FILE__) tiene que coincidir con su propia ruta. La URL de package debería ser un enlace de descarga firmado y de corta duración generado por tu servidor, para que un enlace filtrado deje de funcionar pronto.
Seguridad del servidor de licencias
Si el servidor falla o es vulnerable, los problemas llegan a todos los clientes a la vez. Los puntos mínimos:
- Comunicación solo por HTTPS y validación estricta de cada parámetro en el servidor.
- Limitar la tasa de peticiones por IP y por clave para frenar la adivinación de claves.
- Claves largas y aleatorias, generadas con una fuente criptográficamente segura; no basadas en el correo del cliente ni en fechas.
- Nunca confiar en el cliente: toda decisión de acceso se toma en el servidor, porque el código del plugin es legible y editable.
- Cumplir con las pautas generales de seguridad de plugins también en el módulo cliente: nonces, comprobación de capacidades y escapado.
Experiencia de usuario: fallar con elegancia
Un fallo del servidor de licencias nunca debe romper el sitio del cliente. Esto implica guardar en caché el último estado válido, reintentar con retroceso exponencial, mostrar avisos claros en el panel y degradar de forma suave: si la licencia caduca, se dejan de recibir actualizaciones y soporte, pero el sitio sigue funcionando. Bloquear de golpe funciones que ya estaban en producción es la mejor forma de generar reembolsos y reseñas negativas.
El mismo patrón no es exclusivo de WordPress: nuestro software de marcador Multi Score Screen también envía por correo una clave de licencia por pista, que la instalación activa desde su panel de administración.
También hay que cumplir con la privacidad. Si el cliente envía el dominio, la versión del plugin y la clave, esto debe declararse en la política de privacidad, y no deben enviarse datos personales de visitantes. Basta con lo estrictamente necesario para validar la licencia.
Construir o contratar: cómo decidir
Para un único plugin con pocos planes, un servicio de licencias de terceros o un plugin de licencias ya hecho suele bastar y reduce el tiempo de lanzamiento. Cuando hay varios productos, reglas de plan complejas, integración con facturación propia o requisitos de privacidad estrictos, un servidor a medida compensa a medio plazo. La lógica es la misma que se analiza en la comparativa de software a medida frente a estándar.
El servicio de desarrollo de plugins WordPress de AYM Flow incluye activación, verificación y entrega de actualizaciones, apoyado en la experiencia de mantener nuestro propio producto con licencia, AYM Multilingual. Para quien está empezando el plugin, la guía para crear un plugin de WordPress cubre las bases antes de plantear la capa comercial.
Preguntas frecuentes
¿Se puede proteger el código de un plugin de WordPress con licencias?
No de forma legal ni técnica: los plugins distribuidos deben ser compatibles con GPL y el código es legible. Lo que se controla con licencia es el acceso a las actualizaciones, al soporte y a los servicios en la nube asociados.
¿Cómo se limitan los sitios por licencia?
El servidor registra cada activación con el dominio normalizado y compara el total con el límite del plan. El cliente puede desactivar un sitio para liberar el cupo y trasladarlo a otro.
¿Qué ocurre si el servidor de licencias deja de responder?
El plugin debe seguir funcionando con el último estado válido guardado en caché y reintentar más tarde. Un fallo del servidor nunca debe bloquear el sitio del cliente.
Artículos relacionados
WordPress y webs multilingües
Localización y traducción: la diferencia y cómo aplicarla en tu web
La traducción pasa el texto a otro idioma; la localización reconstruye la web para quien vive en ese mercado. Lo explicamos con ejemplos de nuestra propia investigación de palabras clave.
WordPress y webs multilingües
Añadir un idioma a WordPress: guía paso a paso para una web existente
Quieres añadir inglés u otro idioma a tu web de WordPress. El ajuste “Idioma del sitio” no basta: así se hace con la estructura, las prioridades y los pasos SEO correctos.
WordPress y webs multilingües
Selector de idiomas: diseño usable, accesible y seguro para el SEO
¿Banderas o nombres de idioma? ¿Arriba a la derecha o en el pie? Los principios de un selector de idiomas fácil de encontrar, usable con teclado y comprensible para los buscadores.
