Skip to content

Configuracion y Deployment - Portal de Clientes

DOCUMENTACION RETROSPECTIVA - Generada a partir de codigo implementado el 2026-04-27.

Modulo: Portal de Clientes Tipo: Process Estado: Implementado Fecha: 2026-04-27 Repo frontend: portal-usuarios/Repo backend: bautista-backend/


Resumen del modelo de deployment

  • Frontend: una unica imagen Docker (portal-usuarios) que se build-ea N veces (una por tenant) inyectando variables VITE_* como build args. Cada tenant obtiene su contenedor con su URL de backend, su tenant_id, su sucursal_id y su branding.
  • Backend: instancia compartida de bautista-backend, configurada con PORTAL_ALLOWED_ORIGINS para autorizar CORS desde el portal. Soporta multiples origenes separados por coma.
  • Resolucion multi-tenant: NO hay resolucion DNS por dominio. El frontend declara su tenant_id y sucursal_id en build time; el backend resuelve la conexion via ini.sistema.

Variables de entorno - Frontend (portal-usuarios)

Definidas en portal-usuarios/.env.example. Todas son leidas en build time por Vite (import.meta.env.VITE_*) y quedan inlineadas en el bundle estatico.

VariableRequeridaDescripcionEjemplo
VITE_BACKEND_URLsiURL base de la API backend, sin trailing slash. Usada como baseURL de axios y para CORS.https://api.tenant.com
VITE_TENANT_IDsiID numerico del tenant (empresa). Se manda en cada request via header X-Tenant-Id.1
VITE_SUCURSAL_IDsiID numerico de la sucursal. Se manda via header X-Sucursal-Id y se usa como sucursalId por defecto en login.1
VITE_APP_NAMEnoNombre de la app para branding (titulo del documento, header).Portal de Clientes
VITE_LOGO_URLnoURL absoluta al logo del tenant.https://cdn.tenant.com/logo.png
VITE_PRIMARY_COLORnoColor primario CSS (hex o hsl). Inyectado como --primary en :root.#1e40af
VITE_SECONDARY_COLORnoColor secundario CSS. Inyectado como --secondary en :root.#3b82f6
VITE_THEME_COLORnoColor de la meta tag theme-color para PWA / barra de status mobile.#1e40af

Notas:

  • Validacion de variables obligatorias: el Dockerfile falla el build si VITE_BACKEND_URL, VITE_TENANT_ID o VITE_SUCURSAL_ID no estan definidas.
  • Variables opcionales tienen defaults definidos en BrandingContext.tsx (Portal de Clientes, #1e40af, #3b82f6).
  • Como Vite inlinea las variables en build time, cada tenant requiere un build distinto. No es posible cambiar VITE_BACKEND_URL en runtime.

Variables de entorno - Backend (bautista-backend)

Solo las variables del backend que afectan al portal. Definidas en bautista-backend/.env.dist.

VariableRequeridaDescripcionEjemplo
PORTAL_ALLOWED_ORIGINSsi (cuando portal esta activo)Origenes autorizados para CORS desde el portal de clientes. Separar multiples origenes con coma, sin espacios alrededor de la coma. Cada origen debe coincidir EXACTO con el del frontend (sin barra al final). Usado por PortalCorsMiddleware.https://portal.tenant.com,https://portal2.tenant.com
BACKEND_URLsi (cuando pagos online estan activos)URL publica del backend, sin barra final. Se usa para construir notification_url del gateway: {BACKEND_URL}/backend/portal/pagos/webhook?....https://api.tenant.com

Constantes relacionadas en constants.dist.php:

ConstanteUso
ALLOWED_ORIGINSOrigenes permitidos para el ERP principal. NO se usa para el portal -- el portal tiene su propio middleware con PORTAL_ALLOWED_ORIGINS.
HOST, PORT, USER, PASSWORD, DB_INIConexion a la DB principal (ini.sistema) que resuelve la conexion por tenant.

Notas:

  • PORTAL_ALLOWED_ORIGINS admite multiples origenes separados por coma (ej: https://portal-a.com,https://portal-b.com,http://localhost:5174). Esto permite un backend compartido entre multiples portales con dominios distintos sin modificar el middleware.
  • BACKEND_URL debe apuntar a una URL accesible por PayPerTIC/los gateways. Si falta, el inicio de pago falla rapido con MISCONFIGURED (HTTP 500) antes de crear pagos o llamar al gateway.
  • El backend usa JWT con claves RSA. Las claves estan fuera del scope del portal (son globales del backend). Ver bautista-backend/CLAUDE.md para private-key.pem / public-key.pem.

data_config - Configuracion en base de datos

Nota: la documentacion existente en index.md del modulo menciona que el backend resuelve tenant_id -> DB via ini.sistema. El operador del ERP configura los gateways de pago y otros parametros del portal desde las pantallas de configuracion del ERP.

La estructura concreta de las claves portal.* en la tabla de configuracion (gateway, caja_id, etc.) no fue verificada en el codigo durante esta documentacion retrospectiva. Para evitar inventar contratos, se deja documentada la existencia del mecanismo y se marca como pendiente.

Pendiente de validacion:

  • Listado completo de claves portal.* en data_config (tipos, valores admitidos, defaults).
  • Mecanismo exacto por el cual el backend resuelve tenant_id desde ini.sistema.
  • Referencias cruzadas a las pantallas de configuracion del ERP donde se setean estas claves.

Una vez validado, este apartado deberia documentarse en una doc tecnica backend dedicada al subsistema de configuracion del portal.


Docker - Frontend

Construccion de la imagen

Definida en portal-usuarios/Dockerfile. Build multi-stage:

StageBaseProposito
buildernode:22-alpinenpm ci, copiar fuentes, validar build args, ejecutar npm run build (genera /app/dist).
runnernginx:alpineCopia /app/dist a /usr/share/nginx/html, copia nginx.conf, expone puerto 80.

Build args declarados (todos VITE_*): VITE_BACKEND_URL, VITE_TENANT_ID, VITE_SUCURSAL_ID, VITE_APP_NAME, VITE_LOGO_URL, VITE_PRIMARY_COLOR, VITE_SECONDARY_COLOR, VITE_THEME_COLOR.

Validaciones de build (fallan el build si falta el arg):

  • VITE_BACKEND_URL
  • VITE_TENANT_ID
  • VITE_SUCURSAL_ID

SPA routing en nginx

Definido en portal-usuarios/nginx.conf:

ReglaComportamiento
location / con try_files $uri $uri/ /index.htmlFallback a index.html para rutas SPA. Permite que TanStack Router maneje rutas como /dashboard, /deudas, etc. sin que nginx devuelva 404.
location ~* \.(js|css|woff2|png|svg|ico|jpg|webp)$Cache-Control: public, immutable; expires 1y. Los assets de Vite tienen hash en el nombre, por eso se pueden cachear inmutablemente.
location = /index.htmlCache-Control: no-cache, no-store, must-revalidate; Pragma: no-cache; Expires: 0. El entry point NUNCA se cachea, asi cada deploy entra inmediatamente.

Bundles definidos como manualChunks en vite.config.ts (separan vendor y features para cache eficiente):

  • vendor-react, vendor-query, vendor-router
  • feature-auth, feature-deudas, feature-pagos, feature-cupones

Docker - Deployment por tenant

Modelo

Una sola imagen base, N contenedores (uno por tenant). Cada contenedor se construye con sus propios build args.

TenantBuild args distintosResultado
Tenant AVITE_BACKEND_URL=https://api.bautista.com, VITE_TENANT_ID=1, VITE_SUCURSAL_ID=1, branding AImagen portal-tenant-a:latest
Tenant BVITE_BACKEND_URL=https://api.bautista.com, VITE_TENANT_ID=2, VITE_SUCURSAL_ID=3, branding BImagen portal-tenant-b:latest

Implicancias

  • Cada tenant requiere su propio build (los VITE_* son inlineados por Vite).
  • Una sola imagen no puede servir multiples tenants -- la VITE_BACKEND_URL, VITE_TENANT_ID y VITE_SUCURSAL_ID quedan fijas en build.
  • El backend (bautista-backend) se comparte. Cada portal apunta al mismo backend con sus headers X-Tenant-Id / X-Sucursal-Id.

Resolucion multi-tenant en el flujo de request

Documentada en index.md del modulo. Resumen:

  1. El frontend manda X-Tenant-Id, X-Sucursal-Id y (cuando esta autenticado) Authorization: Bearer {access_token}.
  2. El backend resuelve tenant_id -> base de datos via ini.sistema.
  3. El backend resuelve sucursal_id -> schema PostgreSQL (suc0001, suc0002, etc.).
  4. El JWT lleva tenant_id y sucursal_id -- el backend valida que coincidan con los headers (proteccion contra tampering).

Infraestructura de orquestación por tenant

La infraestructura de deploy está en portal-usuarios/tenants/ y portal-usuarios/scripts/:

portal-usuarios/
├── tenants/
│   ├── .gitignore               # ignora */  (configs reales), versiona _template/ y README
│   ├── README.md                # guía de operación
│   └── _template/
│       ├── .env.example         # 11 variables: DOMAIN, PORT, TENANT_SLUG, 3 VITE obligatorias, 5 VITE branding
│       ├── docker-compose.yml   # build context ../.. , 8 build args VITE_*, ${PORT}:80
│       └── apache-vhost.conf.tpl # VirtualHost con tokens {{DOMAIN}}, {{PORT}}, {{SLUG}}
└── scripts/
    ├── portal-new-tenant.sh     # crea tenants/<slug>/ desde el template con puerto auto-detectado
    └── portal-deploy.sh         # docker compose up + apache graceful reload

Flujo para agregar un tenant nuevo:

bash
# 1. Crear la carpeta del tenant (porta auto-incrementa el puerto)
./scripts/portal-new-tenant.sh <slug>

# 2. Editar tenants/<slug>/.env con las variables reales del tenant
#    (VITE_BACKEND_URL, VITE_TENANT_ID, VITE_SUCURSAL_ID, branding)

# 3. Deployar
./scripts/portal-deploy.sh <slug>

Las configuraciones reales de cada tenant (tenants/<slug>/) están en .gitignore (contienen credenciales y datos de producción). Solo el template _template/ y el README están versionados.

La detección de puerto libre la hace portal-new-tenant.sh leyendo PORT= de los .env existentes en tenants/*/ — el compose usa ${PORT}:80 como variable.


Setup local de desarrollo

Pasos minimos para levantar el portal localmente, derivados de package.json y los archivos de config:

PasoComando / accionResultado
1Clonar portal-usuarios y entrar al directorio--
2Copiar .env.example -> .env.local y completar al menos VITE_BACKEND_URL, VITE_TENANT_ID, VITE_SUCURSAL_IDVariables Vite disponibles
3npm ciInstala dependencias declaradas en package-lock.json
4Levantar backend en paralelo (bautista-backend) con PORTAL_ALLOWED_ORIGINS=http://localhost:5173 (puerto default de Vite)CORS habilitado para el frontend local
5npm run devVite dev server en http://localhost:5173

Otros scripts utiles definidos en package.json:

ScriptComandoUso
buildtsc -b && vite buildBuild de produccion
previewvite previewServir el build localmente
testvitest run --coverageTests unitarios + cobertura
test:watchvitestTests en watch mode
test:e2eplaywright testTests E2E
linteslint src --max-warnings 0Lint estricto
formatprettier --write src testsFormat
type-checktsc --noEmitType check sin emitir

Notas:

  • Para que la cookie portal_refresh_token funcione en local, el backend tiene que servirse con HTTPS (Secure) o el atributo Secure debe relajarse para desarrollo. Actualmente la cookie es siempre Secure; SameSite=None (ver PortalAuthController::setRefreshCookie). Pendiente de validacion: como se maneja el setup local con HTTP. Posibles caminos: mkcert para certificados locales, localhost exempt en algunos browsers, o un override de la cookie en entorno dev.
  • Las variables VITE_* opcionales de branding pueden quedar vacias en local; BrandingContext tiene defaults sensatos.

Notas y pendientes

  • HTTPS en local: la cookie Secure; SameSite=None no funciona sobre HTTP. Falta documentar el flujo recomendado para desarrollo local.
  • Multi-portal con backend compartido: PORTAL_ALLOWED_ORIGINS acepta lista de origenes separados por coma. Un backend puede servir multiples portales con dominios distintos sin configuracion adicional.
  • Claves portal.recibo.*: portal.recibo.cuenta_bancaria y portal.recibo.caja_schema se configuran desde ERP → Config → Gateway de Pagos. Ver Auto-reconciliación técnico.

Ver tambien


NOTA IMPORTANTE: Documentacion retrospectiva. Validar con stakeholders antes de considerarla final. Las secciones marcadas como "Pendiente de validacion" requieren confirmacion del equipo de DevOps / backend.