PROYECTO PERSONAL·EN DESARROLLO ACTIVO·2026
Noctis Commerce
Un SaaS multi-tenant que estoy construyendo para gestión comercial de PYMEs — punto de venta, facturación electrónica SRI e inventario, diseñado para que una sola base de código pueda servir a muchos negocios aislados.
01 — CONTEXTO Y PRODUCTO
Una plataforma para ventas, facturación e inventario.
Un SaaS multi-tenant para comercio, hecho para que una sola base de código opere muchos negocios aislados — cada uno con sus empresas, sucursales y bodegas. Unifica catálogo, punto de venta, inventario valorizado multi-bodega, compras y cartera de clientes/proveedores; la facturación electrónica SRI y la nómina IESS están modeladas y vienen después. En desarrollo activo, no es un producto lanzado.
Para el dueño-administrador —y sus cajeros, bodegueros y personal administrativo— que hoy reparten la operación entre un POS, hojas de cálculo y trámites del SRI por separado.
02 — ARQUITECTURA
Clean Architecture en un monorepo Nx
Cuatro apps desplegables y trece librerías, repartidas en cuatro bounded contexts — identity, auth, commerce y hr. Las dependencias apuntan hacia adentro: el dominio no sabe nada del framework. El modelo de datos abarca 49 modelos Prisma y 56 migraciones solo hacia adelante.
03 — AISLAMIENTO DE TENANTS
Aislamiento en la base de datos, no en la aplicación.
Cada request fija su tenant por transacción; la RLS de PostgreSQL — 37 políticas deny-by-default sobre 36 tablas — filtra cada fila antes de salir de la base. Un tenant sin fijar devuelve nada, no los datos de otro.
-- bind the tenant per transaction (tx-local, parameter-bound) SELECT set_config('app.current_tenant', $tenant, true); ALTER TABLE commerce.products ENABLE ROW LEVEL SECURITY; ALTER TABLE commerce.products FORCE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON commerce.products USING (public.is_platform_admin() OR tenant_id = public.current_tenant_id()); -- unset GUC → current_tenant_id() IS NULL → deny by default
04 — EVIDENCIA
Los números, con lo que significan.
05 — DECISION RECORD
Adoptar Row-Level Security de PostgreSQL en modo force + deny-by-default. Cada request fija app.current_tenant por transacción vía set_config; la policy de cada tabla operacional filtra con is_platform_admin() OR tenant_id = current_tenant_id().
Cada fila operacional lleva un tenant_id. Filtrar solo por convención estaba a un WHERE olvidado — o a una query raw / psql — de una fuga entre tenants, el riesgo de mayor severidad en un SaaS de base compartida.
El aislamiento se sostiene en la base aunque el código de aplicación olvide su filtro; un tenant sin fijar devuelve cero filas, no los datos de otro. Costo: un GUC por transacción y migraciones conscientes de RLS, compatible con el transaction pooling de RDS Proxy — aceptado.