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.

ROL
Proyecto en solitario — arquitectura y build
STACK
NestJS · Next.js 14 · PostgreSQL · Nx
REPOSITORIO
Privado — arquitectura mostrada aquí
LA APP POR DENTRO

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.

PARA
Comercio de cualquier tamaño — de PYMEs a grupos multi-empresa y multi-sucursal.

FACTURACIÓN SRI
Alcance: En curso

MODELO
SaaS multi-tenant
POS · tablet
Total$ ——
Cobrar
Punto de venta — cobro en mostrador
Facturación
$ ——
SRI ✓
Factura electrónica SRI
Inventario
128
34
6
0
Inventario — stock en tiempo real

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.

apps/ · 4 desplegables + 3 e2e
apicommercebackofficeweb+3 e2e
libs/ · 13 librerías · 4 bounded contexts
identity
Domain · users, tenants
Application · permissions, org
Infrastructure · Prisma, RLS
auth
Domain · sessions, policy
Application · JWT, argon2id
Infrastructure · guards
commerce
Domain · products, sales
Application · POS, SRI, stock
Infrastructure · repos, outbox
hr
Domain · employees
Application · labor params
Infrastructure · repos
PostgreSQLRow-Level Security · deny-by-default

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.

request
JWT → RequestContext
set_config
app.current_tenant · por tx
tenant_isolation
deny-by-default
-- 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.

~2,000
Casos de test
Unit por capa, E2E de API contra PostgreSQL real bajo el rol restringido y E2E de UI con Playwright — todo corre en CI.
37
Políticas RLS
Row-Level Security deny-by-default sobre 36 tablas, verificada por una suite E2E que corre bajo el mismo rol restringido.
86
Endpoints REST
Expuestos en 21 controladores, divididos por bounded context — cada uno detrás de un guard de permisos.
75
Permisos atómicos
RBAC fail-closed en 14 módulos — 7 plantillas de rol copiadas por tenant, con resolución cacheada en Redis.
19
Decision records
Decisiones no obvias escritas con sus trade-offs.
Estado actual de un proyecto en desarrollo activo — una foto del código, no un resultado final.

05 — DECISION RECORD

ADR-002
Aislamiento de tenants — Row-Level Security + RequestContext
Aceptado · enmendado 2026-06-10
DECISIÓN

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().

CONTEXTO

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.

CONSECUENCIA

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.