analytics
Portafolio de Iván
Proyectos chevron_right Plataforma B2B multi-tenant

Sistema en producción · B2B curado

lock_person RLS en la base de datos payments Dinero en centavos verified_user 2FA science Vitest + Playwright

Plataforma B2B de experiencias turísticas multi-tenant

No es un marketplace abierto: es una plataforma donde solo entran agencias aprobadas, cada una viendo exclusivamente lo suyo. El aislamiento no se confía al código de la aplicación, se aplica en la base de datos.

Cifras clave

RLS

aislamiento aplicado por PostgreSQL, no por la aplicación

0

importes en coma flotante: todo en centavos

1 rol

con permiso de saltarse RLS, y solo para autenticación

200 → ∞

el truncado silencioso de listados pasó a paginación por cursor

Stack

Next.js 15 (App Router)TypeScript estrictoTailwind 4shadcn/uiPrismaPostgreSQL 16 con RLSBetter Auth + 2FARedisResendVitest + Playwright
schema

Dónde vive cada garantía

Cada regla está puesta en la capa más profunda posible. Cuanto más abajo vive una garantía, menos formas hay de saltársela por accidente.

database Base de datos

Row Level Security

PostgreSQL decide qué filas puede ver cada inquilino. Una consulta mal escrita no puede ver de más.

key Excepción

Un solo rol privilegiado

Existe un rol que puede saltarse RLS y está restringido al adaptador de autenticación.

payments Dominio

Dinero en centavos

Nunca en coma flotante: los importes son enteros y no acumulan error de redondeo.

policy Pruebas

El voucher no filtra al proveedor

Un test automatizado verifica que el documento del cliente jamás muestre datos del proveedor.

list API

Paginación por cursor obligatoria

Sustituye al truncado silencioso a 200 filas que tenía la primera versión.

store

Contexto

Una empresa privada opera una red curada de experiencias turísticas: agencias de viajes previamente aprobadas cotizan, reservan y pagan actividades operadas por terceros. No cualquiera se registra y no cualquiera ve todo; ese filtro es justamente el producto.

Eso significa que en la misma base de datos conviven inquilinos que no deben verse entre sí, y proveedores cuyos datos comerciales no deben llegar al cliente final aunque aparezcan en el mismo flujo de reserva.

El sistema maneja además dinero: cotizaciones, reservas y pagos. Cualquier error de redondeo o de visibilidad no es un bug estético, es una disputa comercial.

report

El problema

En multi-tenant, la pregunta no es si el código filtrará datos entre inquilinos, sino cuándo.

Confiar el aislamiento a filtros escritos en cada consulta funciona hasta que alguien escribe una consulta nueva a las siete de la tarde de un viernes. No hace falta mala fe: basta con olvidar un filtro por inquilino, y en ese momento uno ve la operación de otro.

El mismo riesgo aparece en los documentos. Un voucher se arma con datos de la reserva, y en la reserva está el proveedor. Si el documento se genera «con lo que haya», la información del proveedor llega al cliente final sin que nadie lo haya decidido.

Y había una deuda concreta de la primera versión: los listados se truncaban en silencio a 200 filas. Quien consultaba un periodo con más movimientos recibía una respuesta que parecía completa y no lo era, que es la peor forma de estar equivocado.

rule

Decisiones clave

Cada una de estas reglas está escrita como no negociable en el propio repositorio, para que sobreviva a quien las escribió.

01 El aislamiento se aplica en la base de datos

PostgreSQL tiene Row Level Security activo: es el motor el que decide qué filas puede ver cada inquilino, no la capa de aplicación.

psychology Porque una regla que vive en el código depende de que todas las consultas la recuerden, y basta una para romperla. Si vive en la base, la consulta olvidadiza simplemente no obtiene los datos ajenos.

02 Una sola excepción, y acotada

Hay un rol de PostgreSQL con permiso para saltarse RLS, y su uso está restringido exclusivamente al adaptador de autenticación.

psychology Toda regla de seguridad real necesita una puerta para el proceso que valida identidades. Lo importante es que esa puerta sea una, esté nombrada y no se preste para nada más; una excepción difusa acaba usándose «solo esta vez» en media aplicación.

03 El dinero es un entero

Todos los importes se guardan y se operan en centavos, nunca en coma flotante.

psychology Los números con decimales en coma flotante no representan exactamente los importes de dinero y acumulan diferencias al sumar. En un sistema donde el cliente compara su total con el del proveedor, esa diferencia de un centavo termina en una llamada telefónica.

04 Que el voucher no filtre, comprobado por una prueba

Un test automatizado verifica que el documento que recibe el cliente jamás incluya datos del proveedor. Además, los campos sensibles van cifrados.

psychology Es la misma lógica de todo el sistema: una regla que solo existe en la cabeza del equipo desaparece con el equipo. Escrita como prueba, cualquier cambio futuro que la rompa se detiene antes de llegar a producción.

05 Paginar por cursor, obligatoriamente

La segunda versión eliminó el truncado silencioso a 200 filas y exige paginación por cursor en los listados.

psychology Un listado incompleto que se presenta como completo hace que alguien tome decisiones con la mitad de los datos y sin saberlo. Prefiero obligar a paginar —más trabajo para quien consume la API— que devolver una mentira cómoda.

construction

La solución

La aplicación es Next.js 15 con TypeScript estricto sobre PostgreSQL, con autenticación propia que soporta segundo factor, caché en Redis y correo transaccional para las confirmaciones. Los documentos de reserva se generan desde la misma aplicación.

La parte interesante no es el marco de trabajo, sino dónde está puesta cada garantía: el aislamiento en el motor de base de datos, el dinero en enteros en el modelo de dominio, la confidencialidad del proveedor en una prueba automatizada, y la integridad de los listados en el contrato de la API.

Las pruebas van en dos niveles: unitarias para las reglas de dominio y de extremo a extremo para los flujos completos de cotización, reserva y pago.

emoji_events

Resultados

lightbulb

Lo que aprendí

Que la seguridad multi-tenant se gana o se pierde en la capa más profunda. Todo lo que se resuelve en la base de datos deja de depender de la disciplina de quien escribe la siguiente consulta.

Que escribir las reglas como «no negociables» dentro del repositorio no es burocracia: es lo que hace que sigan vivas cuando el proyecto cambie de manos.

Y que una respuesta incompleta que aparenta ser completa es más peligrosa que un error. El error se ve; el listado truncado se cree.