Saltar al contenido
Insights
Ingeniería/5 min/17 de enero de 2026

Multi-tenant sin magia: cómo un mismo backend sirve múltiples clientes

El header x-client tiene cuatro caracteres. Lo que hace detrás es lo que distingue una arquitectura de un workaround.

La pregunta que más recibimos cuando explicamos la arquitectura de Pegasuz es directa: "¿cómo sabe el backend qué cliente está hablando?"

La respuesta en una línea: un header HTTP llamado `x-client`. Cada request que llega al API lo incluye con el identificador único del cliente — su slug. El middleware intercepta ese valor, resuelve todo lo que necesita saber sobre ese tenant, y el resto del sistema trabaja como si fuera un backend dedicado.

Pero la simplicidad de la interfaz esconde bastante trabajo detrás.

01

El flujo completo, paso a paso

Cuando llega una request a `POST /api/posts` con el header `x-client: gentile`, esto es lo que sucede antes de que el controller vea el request:

**1. clientResolver.middleware** intercepta el request, lee el header x-client, busca el registro del cliente en la base de datos central (pegasuz_core), y resuelve la URL de la base de datos del tenant (algo como `mysql://user:pass@localhost:3306/pegasuz_gentile`).

**2. Se instancia un cliente Prisma** con esa URL específica. No un cliente compartido — uno nuevo, apuntando exactamente a la base de datos de ese tenant.

**3. El cliente Prisma se inyecta en el objeto req** de Express. `req.prisma` ahora es una conexión aislada al tenant correcto.

**4. El controller recibe el request**, llama a `req.prisma.post.create({...})`, y crea el registro en la base de datos del tenant correcto. Sin saber quién es el tenant. Sin condicionales. Sin magia.

02

Por qué separamos por base de datos y no por tenant_id

Hay dos modelos clásicos de multi-tenancy. El primero es shared schema: todas las entidades tienen una columna tenant_id, y cada query filtra por ese campo. El segundo es isolated schema: cada tenant tiene su propia base de datos.

Elegimos el segundo modelo. Las razones son concretas.

**Seguridad estructural**: en el modelo tenant_id, la separación depende de que cada query filtre correctamente. Un solo error — un join sin filtro, un middleware que no se ejecuta, un bug de lógica — puede exponer datos de todos los clientes. Con bases de datos separadas, la separación es física. Un bug puede romper funcionalidad, pero no puede filtrar datos entre tenants.

**Operación independiente**: podemos hacer backup, restaurar o migrar un cliente sin tocar a los demás. Si un cliente necesita más recursos, se puede mover a un servidor dedicado sin afectar al resto.

**Esquemas independientes**: si un cliente necesita un campo extra en una tabla, podemos agregarlo en su base sin afectar a los demás. No siempre lo hacemos — la consistencia tiene valor — pero la opción existe.

03

El overhead real

Más bases de datos significa más conexiones. Instanciar un cliente Prisma por request tiene un costo. Lo mitigamos con connection pooling y caching de instancias por tenant, pero el overhead existe.

Las migraciones también son más complejas. Cuando actualizamos el schema, ejecutamos la migración en cada base de datos de tenant por separado. Tenemos scripts que automatizan eso, pero es operación adicional.

Para el volumen que manejamos — decenas de requests por minuto por cliente — el overhead es aceptable. Si escaláramos a miles de tenants con millones de requests simultáneos, probablemente reconsideraríamos algunas decisiones.

04

El sistema de features

Además de la separación de datos, cada tenant tiene un registro de features habilitadas. El campo features en la tabla de clientes define qué módulos están activos: blog, ecommerce, properties, portal, etc.

El middleware `featureGuard` verifica esa lista antes de cada endpoint. Si un tenant no tiene ecommerce habilitado y llega una request a `/api/products`, el guard devuelve 403 antes de que el controller vea nada.

Esto significa que el mismo backend sirve a una inmobiliaria (con properties, contracts, portal), a una tienda (con products, variants, orders), y a un portfolio profesional (con projects, services, blog) — sin condicionales en el código de negocio.

05

Lo que hace que funcione en producción

El clientResolver necesita encontrar el tenant en la base de datos central en cada request. Eso es una query extra antes de cada operación. El resultado se cachea en memoria por un tiempo configurable — suficiente para que requests consecutivos del mismo tenant no vayan a la DB dos veces.

Si el header x-client llega vacío, corrupto o apuntando a un tenant inexistente, el middleware rechaza el request con 401. No hay fallback a ningún tenant por defecto.

Es la decisión que hace que el sistema sea seguro: un request sin identidad no existe para el backend.

Takeaways
01

La capa editorial necesita estructura, no solo publicacion.

02

Metadata, taxonomias y locale afectan el valor futuro del contenido.