⚖️ Infraestructura

Despliegue multiinstancia

Ejecuta cualquier número de réplicas del servidor tenant detrás de un balanceador de carga sin restricciones de "debe estar en el mismo pod". Cada componente del servicio está diseñado para ser sin estado — el estado compartido reside en Postgres y Redis.

💡
En resumen

Apunta TENANT_BASE_URL a tu balanceador de carga. Todas las réplicas comparten el mismo Postgres y Redis. No se requieren sesiones persistentes para la voz. El worker del agente de personalización puede acceder a cualquier réplica.

Sin estado por diseño

El servidor tenant no mantiene ningún estado en memoria que deba sobrevivir a una redirección a otra réplica. Las claves de firma de JWT residen en Postgres (en el espacio de nombres livekit_server) y en tu passwords.yaml de despliegue. Cada réplica lee desde la misma fuente, por lo que cualquier pod puede emitir un JWT de LiveKit para cualquier usuario.

El agente de personalización (worker de voz/avatar) llama de vuelta a las rutas /internal/customization-agent/*. Estas rutas buscan al usuario de la organización en Postgres, validan el tenant y continúan — no se consulta ningún estado en memoria por réplica. Apunta TENANT_BASE_URL al balanceador de carga y cualquier réplica puede responder.

Cómo funciona la coordinación entre réplicas

En cualquier lugar donde las réplicas podrían interferir entre sí bajo balanceo de carga, YOffice coordina a través de Redis. Cada caso de uso se asigna a un primitivo distribuido que mantiene todas las réplicas sincronizadas:

Caso de coordinaciónPor qué importa entre réplicasCómo lo gestiona YOffice
Barridos periódicos en server.dartSin coordinación, cada réplica ejecutaría cada barrido → N× el trabajo y disparadores duplicadosLeaderOnlyTimer.periodic(taskName: …) — un líder elegido por Redis ejecuta el barridoun líder elegido por Redis ejecuta el barrido
Limitación de solicitudes por organizaciónUn contador en memoria permitiría que cada réplica aplicara el límite en su propia porción, de modo que N réplicas permitirían N× el límiteDistributedRateLimiterINCRBY atómico en Redis por tenant + segmento de tiempo
Límite de solicitudes por IP para formularios públicosLas solicitudes podrían eludir el límite al llegar a réplicas distintasDistributedRateLimiter con clave IP + segmento
Concurrencia de embeddingsUn semáforo por réplica se multiplicaría por N réplicas y superaría las cuotas del proveedorDistributedSemaphore con capacidad global del clúster
Invalidación de caché de directivasUna invalidación solo local dejaría a otras réplicas sirviendo directivas obsoletas hasta 5 minutosContador de generación en Redis — la invalidación de cualquier réplica lo incrementa; todas las réplicas ven el cambio
Distribución de pub/sub en tiempo realpostMessage sin solo llega a los suscriptores de la réplica que publicóglobal: true reaches only the publishing replica's subscribersLos 137 puntos de llamada usan ClusterPubsub.broadcast (global forzado)

Primitivos de escalado

Todos los primitivos están en command_center_tenant_server/lib/src/scaling/horizontal_scaling.dart. Solo requieren un clúster de Redis compartido — no se necesita ningún servicio de coordinación adicional.

  1. DistributedLock

    Bloqueo exclusivo Redis SET NX PX. Usado para coordinar la exclusión mutua entre réplicas. Expira automáticamente tras un TTL configurable para que un propietario caído no bloquee el clúster indefinidamente. Los titulares de larga duración deben llamar a renew() dentro del período TTL.

  2. LeaderOnlyTimer

    Un reemplazo directo de Timer.periodic que usa un contrato de líder Redis por tarea. Solo la réplica líder elegida ejecuta el cuerpo; las demás réplicas permanecen inactivas. Cada tarea elige a su propio líder de forma independiente, distribuyendo la carga de forma natural entre los pods.

  3. DistributedSemaphore

    Un semáforo contador que limita la concurrencia global del clúster con claves de ranura en Redis. Reemplaza los semáforos por aislamiento que solo aplican límites localmente — importante para los embeddings masivos de carpetas de conocimiento y operaciones similares sensibles a cuotas de proveedor.

  4. DistributedRateLimiter

    INCRBY atómico en Redis con clave por tenant + segmento de tiempo. Aplica los límites de solicitudes por organización en todas las réplicas simultáneamente. Las configuraciones por organización se escriben desde la pantalla de administración de límites y se envían a Redis en ~30 segundos.

  5. ClusterPubsub

    Un envoltorio ligero sobre session.messages de Serverpod que pasa global: true para que los eventos pub/sub lleguen a los suscriptores de todas las réplicas, no solo de la que publicó. Todos los eventos de chat en tiempo real, presencia y flujos de trabajo usan este envoltorio.

Topología de despliegue

Una configuración de producción típica tiene este aspecto:

Clients (Flutter / browser)
        │  HTTPS / WSS
  Load balancer (nginx / Cloudflare / ALB)
     │                   │
tenant-server pod   tenant-server pod   … × N
     │                   │
     ├── Postgres (shared)
     ├── Redis (shared)
     ├── LiveKit Cloud
     └── MinIO / Supabase Storage

customization-agent worker(s)
  └── HTTP callbacks → TENANT_BASE_URL/internal/…
      (any pod can answer)

Lista de verificación de despliegue

  1. Apuntar TENANT_BASE_URL al balanceador de carga

    Establece TENANT_BASE_URL con el nombre de host de tu balanceador de carga (p. ej. https://tenant.tu-dominio). Cada callback del worker del agente de personalización — /internal/customization-agent/* — incluye tenantId, organizationUserId y una cabecera secreta. Cualquier réplica puede responder; no se requiere ningún estado en memoria de la réplica que emitió el JWT.

  2. Configurar Postgres y Redis compartidos

    Todas las réplicas deben leer desde la misma instancia de Postgres y el mismo clúster de Redis. Los primitivos distribuidos (bloqueos, elecciones de líder, semáforos, límites de solicitudes, pub/sub) solo funcionan correctamente cuando todos los pods comparten un único espacio de claves de Redis.

  3. Activar sesiones persistentes para WebSocket (opcional pero recomendado)

    Las sesiones persistentes no son necesarias para la voz — LiveKit Cloud conecta a los clientes directamente con su propio plano de medios tras la emisión del JWT. Las sesiones persistentes SÍ son útiles para el WebSocket de Serverpod, para que los clientes de chat no se reconecten repetidamente al reiniciar un pod. Usa ip_hash en nginx o una política de afinidad de sesión en un balanceador de carga gestionado.

  4. Establecer CC_INSTANCE_ID por pod (opcional)

    Cada pod deriva una identidad estable de CC_INSTANCE_ID → HOSTNAME → nombre de host de la plataforma → fallback aleatorio. Establecer CC_INSTANCE_ID explícitamente en tu manifiesto de despliegue te da nombres de pod coherentes en los registros, tokens de propietario de bloqueos distribuidos y la ruta de depuración /internal/instance-info.

Voz y LiveKit Cloud

ℹ️
No se necesitan sesiones persistentes para la voz

LiveKit Cloud conecta a los clientes directamente con su propio plano de medios WebRTC después de que el servidor tenant emite un JWT. El servidor tenant no está en la ruta de medios tras la emisión del token, por lo que la afinidad de sesión no afecta a la calidad de voz.

El worker del agente de personalización se registra en LiveKit Cloud usando el identificador agent_name=customization. Envía callbacks a TENANT_BASE_URL — tu balanceador de carga — y cualquier réplica puede gestionarlos. El propio worker también puede ejecutarse como múltiples instancias; cada instancia se registra en LiveKit de forma independiente.

Límites de solicitudes por organización

Los administradores de la organización pueden ajustar el límite de solicitudes por organización en Ajustes → Límites de solicitudes. El nuevo valor se escribe en Postgres y se envía a Redis en ~30 segundos, por lo que todas las réplicas lo recogen sin necesidad de reinicio. El valor por defecto es 240 solicitudes por minuto.Settings → Rate Limits

⚠️
Redis es obligatorio en modo multiinstancia

Los primitivos distribuidos (bloqueos, elecciones de líder, límites de solicitudes, pub/sub) requieren Redis. Si Redis no está disponible, el servidor vuelve al comportamiento en proceso, que es seguro para despliegues de una sola réplica pero mostrará los errores descritos anteriormente bajo balanceo de carga.