Skip to main content

Multi-tenant por empresa

La decisión: Proteus sirve a varias empresas (clientes) desde la misma base de datos. El aislamiento es por columna COMPANYID en las tablas del dominio, se resuelve por sesión y es fail-closed.

Cómo se resuelve el tenant

  1. El login pasa por Ajolote, que resuelve el tenant server-side y lo pinnea en cookie httpOnly. El front nunca conoce la lista de tenants. Ver Autenticación.
  2. El JIT-provisioning persiste el appId en user.company. Del lado de Proteus alcanza con el userId: no hay que threadear datos del request por toda la pila.
  3. CompanyContextService resuelve el UserScope ({ role, companyId }) leyendo de Redis (USERSCOPE-<id>) con fallback a la base, y cachea el mapeo appId → Company.id en memoria.

Los dos sentinelas del fail-closed

export const NO_COMPANY_SCOPE = -1; // "ninguna empresa": WHERE COMPANYID = -1 → 0 filas
Valor de companyIdSignificadoA quién le toca
nullVe TODO (bypass del filtro)Solo SUPERUSER y ADMIN
-1No ve nadaUn usuario no privilegiado sin empresa asignada
un idVe solo esa empresaTodos los demás
Nunca colapses null y -1 a un solo "falsy"

Distinguirlos es el corazón del diseño. null es "ve todo" y -1 es "no ve nada": son opuestos. Un if (!companyId) que los trate igual convierte un usuario sin empresa en un superusuario.

La elección es deliberada: para un usuario no privilegiado sin empresa, preferimos que no vea nada antes que verlo todo.

Alcance

COMPANYID está —o debe estar— en las tablas del dominio:

Patient · AtentionProcess · Authorizations · USERS · Supplies · HospitalBedOccupancy · Company.APPID · y las tablas de los módulos nuevos.

Un paciente por empresa

Desde el cambio de unicidad, un mismo paciente-persona con dos empresas son dos filas de Patient, una por empresa. La clave de unicidad es (tipo de documento + documento + COMPANYID). Ver Pacientes y credenciales.

El síntoma clásico: "no veo nada en la bandeja"

Casi siempre es dato faltante, no un bug de la query

Cuando una bandeja aparece vacía para un usuario no privilegiado, el primer sospechoso es COMPANYID sin poblar en las filas viejas (Authorizations, USERS, Patient…). El filtro fail-closed hace exactamente lo que tiene que hacer y devuelve 0 filas.

La solución es correr el backfill, no tocar la query. Ver DDL y ops.

Gotchas

  • El bypass es solo SUPERUSER y ADMIN (canSeeAllCompanies). Un MODERATOR no ve todo.
  • El scope está cacheado en Redis. Cambiar la empresa de un usuario directamente en la base no se refleja hasta que expire la key USERSCOPE-<id>.
  • Hay un segundo filtro ortogonal: el centro médico activo. Un usuario puede tener la empresa correcta y el centro incorrecto, y el síntoma es idéntico. Cuando diagnostiques un "no veo nada", chequeá los dos.
  • El DDL de las columnas COMPANYID y sus backfills está en migrations/postgres/ddl/, y tiene que correr antes de levantar el código que las usa.