crm

Arquitectura

Contexto y decisiones ya tomadas

Aislamiento multi-tenant: Row-Level Security, no solo código

La decisión de seguridad más importante del proyecto: el aislamiento entre negocios no depende de que el código de la aplicación recuerde añadir WHERE tenant_id = ... en cada consulta. Se aplica a nivel de base de datos con Row-Level Security (RLS) de PostgreSQL:

  1. Cada tabla con datos de un negocio (customers, users, interactions, purchases, returns, points_config, audit_log) tiene una columna tenant_id y una política RLS:
    CREATE POLICY tenant_isolation ON customers
      USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
      WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
    
  2. Antes de cada operación, el backend fija esa variable de sesión al tenant del usuario autenticado (ver PrismaService.withTenant() en backend/src/prisma/prisma.service.ts), dentro de una transacción.
  3. Fail-closed: si la variable no está fijada (código con un bug, o un acceso fuera de este mecanismo), Postgres no devuelve ninguna fila — no hay un “modo por defecto” que muestre todo.
  4. Importante: RLS no protege nada si la conexión usa un rol superusuario, porque estos lo saltan (bypass) por defecto. Por eso existe un rol separado, app_role, creado en la propia migración (NOSUPERUSER NOBYPASSRLS), que es el único que debe usar el backend en producción. El rol administrador de Postgres solo se usa para aplicar migraciones.
  5. La tabla tenants es la única excepción parcial: no contiene datos de clientes, solo el registro del negocio en sí (nombre, slug, tipo), así que su lectura es pública — es necesaria para que el login pueda localizar un negocio por su slug antes de que exista una sesión con tenant fijado. La escritura sigue restringida (alta pública permitida, modificación solo del propio tenant).

Esto se verificó con pruebas reales, no solo revisando el código: conectando como app_role (no como superusuario), se comprobó que:

Después, se repitió la misma prueba a través de la API HTTP completa (registro de dos negocios, creación de clientes en cada uno, y verificación de que ninguno puede leer los datos del otro, ni siquiera pidiendo el ID exacto del recurso de otro tenant — responde 404, no 403, para no revelar que el recurso existe).

Autenticación y autorización

Modelo de datos

Ver backend/prisma/schema.prisma y la migración backend/prisma/migrations/00000000000000_init/migration.sql (fuente de verdad, incluye las políticas RLS que Prisma no modela de forma nativa).

Tablas: tenants, users, points_config, customers, interactions, purchases, returns, audit_log.

customers.custom_fields es JSONB para permitir que cada negocio añada campos propios de su sector sin necesitar una migración de esquema.

Puntos, compras y devoluciones

Segmentos pre-creados

GET /segments (solo owner/admin) calcula los tres segmentos pedidos al inicio del proyecto con una única consulta SQL (segments.service.ts), comparando dos periodos de 12 meses (el actual y el inmediatamente anterior):

Dos definiciones que son elección nuestra, no un estándar universal, y que quedan documentadas en el propio código para que sean explícitas:

Un cliente sin visitas en el periodo anterior (recién dado de alta) no puede tener una media de gasto “anterior”, así que queda excluido de los segmentos de aumento/disminución de gasto (no se puede comparar contra un dato que no existe) — solo puede aparecer en “visitas disminuidas” si además tenía visitas antes y ahora tiene menos, lo cual por definición no le pasa a un cliente nuevo.

Lo que falta por construir