ADR 0016 — Planes y módulos por tenant
- Estado: aceptada
- Fecha: 2026-10-17
Contexto
Las apps SaaS sobre tenancy-node venden planes: cada tenant ve solo los módulos que paga y tiene límites (usuarios, sucursales). Hasta 0.10 el tenant solo tenía un campo plan de texto libre, y cada app repetía la lógica en la API, el menú y la web de precios, que terminaban contradiciéndose.
Decisiones
- Una sola fuente: la configuración.
features(catálogo),plans(módulos, límites, precio,public) ydefaultPlanviven encreateTenancy. De ahí salentenancy.features,requireFeature,/tenancy/me,GET /tenancy/plans, el panel y la CLI. Los planes no se guardan en la base: cambiarlos es un despliegue, versionado con el código. - La librería no conoce módulos concretos. Los nombres los pone cada proyecto. El autocompletado es opcional, con declaration merging (
TenancyFeatureMap,TenancyLimitMap), igual queTenancyEventMap. - Módulos sueltos por tenant en la base central (columna
features:{ add, remove, limits }), para vender "Básico + Reportes" sin crear un plan. Se normalizan (sin repetidos, ordenados) y se validan contra el catálogo al escribir. - Los datos nunca se borran por el plan. Bajar de plan oculta y bloquea; los límites solo impiden crear más (
assertLimitcon el total que cuenta la app, porque la librería no conoce sus tablas). - Tolerante al leer, estricto al escribir. Asignar un plan que no existe es un error (422). Leer un tenant con un plan que ya no está en la configuración no rompe: queda sin módulos de plan y se avisa una vez en el log.
- Sin
plansel control está apagado (todo permitido), así 0.11 se instala sin cambiar comportamiento, ypublicFieldssigue leyendofeatures/limitsdetenant.datacomo antes. - Errores 403 con detalles públicos.
TENANCY_FEATURE_NOT_IN_PLANyTENANCY_PLAN_LIMIT_REACHEDson los únicos errores que incluyendetailsen la respuesta HTTP, para que el frontend muestre "Tu plan permite 3 usuarios". GET /tenancy/planses público (cualquier host, CORS abierto, ETag): lo consume la web comercial, que vive en el dominio central o en otro sitio.- Solicitudes de demo en el panel, no en el núcleo: necesitan base central, límite por IP y gestión con roles. La ruta pública
POST /admin/api/leadsestá apagada por defecto, usa un campo trampa (responde 201 sin guardar) y publicalead.createdpara que la app avise por su canal. - Tipografías permitidas (
theme.fonts): con lista, el tema solo acepta esas familias ytheme.cssincluye el@importde Google Fonts o de la URL propia; sin lista, nada cambia.
Consecuencias
- Al actualizar desde 0.10 hay que correr
tenancy installantes de desplegar (columna nueva). /tenancy/mese cachea (http.cacheControl, 5 min por defecto): un cambio de plan llega al navegador al vencer la caché; la API lo aplica de inmediato.- Cambiar precios exige desplegar. Si hace falta editarlos desde el panel, se puede agregar una capa en la base sin romper esta API.