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.
El dominio no es solo un registro DNS: deben coincidir el vhost, el dominio
registrado en PostgreSQL y los orígenes admitidos por zentto-auth. Si una de
esas tres capas falta, la navegación puede abrir pero el login no queda
operativo.
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:
ssh root@178.104.56.185bash /opt/zentto/alta-dominio-cliente.sh sucliente.com <companyId>El script es idempotente (re-ejecutarlo verifica/repara) y ejecuta el
runbook operativo: DNS → SSL strict → cert → vhost → registro en BD → orígenes
federados en la BD activa → restart API → verificación HTTP. Después hay que
versionar dos resultados para que sobrevivan a despliegues: el .conf generado
en zentto-infra y los orígenes en el seed de zentto-auth. El script no edita
ni commitea esos repositorios. 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).- Las cookies del shell salen con
Domain=.zentto.net(SSO cross-subdominio del SaaS) y el browser las descarta bajo el dominio del cliente → el login muere conMissingCSRFaunque credenciales y broker estén perfectos. El vhost del cliente DEBE llevarproxy_cookie_domain .zentto.net .<dominio>;(ya está en_plantilla.conf; detectado en el Edge Lab, Pulse 627 — afectaba también a San José en prod). - 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.