Dominio propio por tenant (white-label)
Un tenant Enterprise puede operar bajo su propio dominio: el cliente ve
sucliente.com en la barra, no *.zentto.net. Es parte de lo vendido.
Caso de referencia: Repuestos San José (sanjoserepuestos.com, Pulse 625,
2026-09-12).
Cómo funciona
Sección titulada «Cómo funciona»- Resolución de tenant por dominio:
cfg.Company.CustomDomain+usp_cfg_tenant_resolvedomain. El middleware de la API resuelve un host fuera de*.zentto.netprobando el host exacto y sin su primer label (caja.sucliente.com→sucliente.com), con cache. CORS acepta los orígenes de dominios registrados con el mismo cache. - API mismo-origen: bajo un dominio custom,
@zentto/shared-apiusa base relativa ('') — el vhost nginx del dominio proxea/v1,/apiy/media-filesal API. Cero CORS real, cookies first-party. - TenantGuard resuelve por
GET /api/tenants/resolve-domain/:hosty persiste{companyId, slug, domain}. - Caja/Mostrador: el host-lock del POS matchea
caja.*/mostrador.*de cualquier dominio — no requiere cambios por cliente.
Alta en UN comando (desde Pulse 626)
Sección titulada «Alta en UN comando (desde Pulse 626)»Con el tenant ya provisionado y la zona del dominio activa en la cuenta CF:
bash /opt/zentto/alta-dominio-cliente.sh sucliente.com <companyId>El script es idempotente (re-ejecutarlo verifica/repara) y hace todo el
runbook de abajo: DNS → SSL strict → cert → vhost → registro en BD → orígenes
federados (persisten: el seed de zentto-auth hace MERGE) → restart API →
verificación E2E. Al final, versionar el .conf generado con un PR a
zentto-infra (el propio script lo recuerda). Además, desde Pulse 626 el
provision-full siembra también el contenido del tenant (company, branch y
ADMIN dual-store) — ya no hay seed manual de usuarios del pipeline.
Runbook manual (referencia — lo que hace el script)
Sección titulada «Runbook manual (referencia — lo que hace el script)»Prerequisito: el tenant existe (Enterprise, BD dedicada) y el dominio está en la cuenta Cloudflare de Zentto como zona activa (comprado en CF Registrar o delegado por NS).
- DNS (zona del cliente en CF): registros
Aproxied →178.104.56.185para: raíz,www,caja,mostrador. SSL/TLS de la zona en Full (strict). - Cert en el server (antes del vhost — sin cert el sync lo omite):
Ventana de terminal certbot certonly --dns-cloudflare \--dns-cloudflare-credentials /opt/zentto/cloudflare.ini \-d sucliente.com -d '*.sucliente.com' - Vhost: copiar
zentto-infra/nginx/clientes/sanjoserepuestos.com.conf→sucliente.com.conf, reemplazar el dominio, PR adevelopery promoción. El sync prod lo planta solo si el cert existe. - Orígenes federados en zentto-auth (OBLIGATORIO): añadir
https://sucliente.com,https://caja.sucliente.comyhttps://mostrador.sucliente.comaAllowedOriginsde la appzentto-erpen el SEED00000_register_apps.sqldezentto-auth(el seed pisa AllowedOrigins en cada deploy — una migración sola se deshace). Sin esto el login Microsoft/Google bajo el dominio falla. ⚠️ El seed es zona caliente de conflictos entre carriles: coordinar antes de tocarlo. Hecho para San José en zentto-auth#137/#139. - Registrar el dominio en el tenant (master y BD dedicada):
Reiniciar el API (o esperar el TTL del cache de dominios).SELECT * FROM usp_cfg_tenant_setcustomdomain(<companyId>, 'sucliente.com');
- Verificar: raíz y
www→ 200 sirviendo el shell;caja./mostrador.→ 200 en su pantalla con el dominio visible;GET https://sucliente.com/api/tenants/resolve-domain/sucliente.com→ 200; login del ADMIN del tenant bajo el dominio → token; y el botón Microsoft del login → 302 a login.microsoftonline.com (federado del dominio activo).
Trampas conocidas (pagadas en el caso San José)
Sección titulada «Trampas conocidas (pagadas en el caso San José)»- El vhost sin cert tumba el arranque de nginx — por eso el sync lo
condiciona a
/etc/letsencrypt/live/<dominio>/. Cert SIEMPRE primero. /api/auth/va al SHELL conX-Forwarded-Host(no al API): sin ese header Auth.js emite cookies/redirects para el host equivocado (patrón del item 618).- BrandCode y otros varchar cortos: si además migras inventario legacy, ver el runbook del bulk-import (marcas >20 chars se truncan).
- Los usuarios de un tenant viven en TRES stores: ERP master + BD dedicada
(dual-store del ERP) y el broker central
zentto_auth— el login del shell (Auth.js) autentica contra el broker y NO tiene fallback legacy: sin la identidad central el usuario ve “credenciales incorrectas” aunque el ERP esté perfecto (caso San José, Pulse 626). El seed central requiere:auth."User"(username GLOBAL único + hash bcrypt),auth."UserApp"(appzentto-erp),auth."UserCompanyAccess"(CompanyId/BranchId espejo +ErpUserCode/ErpUserId) y la empresa/sucursal enauth."Company"/"Branch"(patrón:sanjose-seed-auth.sql). ⚠️ El username central es global: dos tenants no pueden tener ambos “ADMIN” — pendiente de diseño el login central scoped por tenant. - Bajo un tenant, el selector de empresas del login muestra solo la empresa
del tenant (+DEMO si tiene acceso); ver otras empresas reales es exclusivo
de los superadmin de plataforma (fix en
login-options). - El origen federado vive en el SEED de
zentto-auth, no en una migración (el seed pisaAllowedOriginsen cada deploy) — por eso es el paso 4 del runbook. Verificado en San José: con el seed, el start OIDC bajo el dominio responde 302 a Microsoft.