Ir al contenido

Hecha por ingenieros, para ingenieros.

Arquitectura, seguridad y operación, expuestas para quienes van a integrarse con esta plataforma, construir módulos sobre ella y aprobar la revisión de arquitectura. Aquí no hay nada suavizado para marketing.

dotnet build CleverInit.slnx

0 Warning(s) · 0 Error(s)

dotnet test CleverInit.slnx

4,028 passed · 0 failed

4028
Pruebas automatizadas, solución host
0
Avisos de compilación tolerados
5
Behaviors en cada comando
5
Pasos para resolver un tenant

Cada decisión de arquitectura es intencionada. Nada está incluido por accidente.

Diez decisiones que marcaron todo lo demás.

Cada una de ellas se impone en el código o se comprueba en CI, no se escribe en un wiki y se confía en la suerte.

  1. 01

    Clean Architecture impuesta por el compilador

    Dominio ← Aplicación ← Infraestructura ← API son referencias entre proyectos, no convenciones. Un handler que alcanza la infraestructura rompe la compilación.

  2. 02

    Un único pipeline para cada petición

    Registro, trazas, validación, invalidación de caché, transacción: siempre en el mismo orden, de modo que los tiempos, la validación y la semántica de errores se mantienen uniformes en todos los endpoints.

  3. 03

    Una base de datos por cliente registrado

    Una cuenta creada a través del registro recibe su propia base de datos SQL, y su cadena de conexión se cifra mediante ASP.NET Core Data Protection antes de almacenarse.

  4. 04

    Las lecturas son proyecciones con contrato de paginación

    Toda lectura es una proyección sin seguimiento a un DTO, y todo endpoint de listado devuelve un resultado paginado con un tope estricto. Un conjunto de resultados sin límite no es una opción que ofrezca el código.

  5. 05

    Nada de SQL en crudo, en ningún sitio

    Solo consultas parametrizadas de EF Core. La entrada del usuario nunca se interpola en SQL, porque el camino de construcción de cadenas sencillamente no existe.

  6. 06

    Eventos que sobreviven a una caída, con semántica honesta

    Lo que hay que anunciar se guarda en la base de datos junto al propio trabajo y luego lo publica un único barredor. La entrega es al menos una vez, los reintentos son finitos y los mensajes muertos se declaran, no se esconden.

  7. 07

    Las migraciones solo se añaden

    Una migración ya publicada no se edita nunca. Cada cambio de esquema es una nueva migración hacia delante, en el mismo commit que el cambio de entidad que la necesita.

  8. 08

    Las API están versionadas

    La superficie HTTP funciona sobre Asp.Versioning, así que un cambio incompatible es una versión nueva y explícita, no una edición silenciosa bajo su integración.

  9. 09

    Los errores siguen RFC 7807, en lenguaje claro

    La validación se traduce en un 400 con un mapa de errores por campo, lo no encontrado en 404, los duplicados en 409, las reglas de negocio en 422, y el detalle es siempre una frase completa, nunca un nombre de clase ni un fragmento de SQL.

  10. 10

    Los módulos tienen que demostrar que se descargan

    Cargar un módulo, soltar todas las referencias, forzar una recolección de basura y comprobar que la referencia débil a su contexto de carga está muerta. Si un módulo no puede descargarse de verdad, esa comprobación falla.

Cuatro capas, una sola dirección.

Las flechas de dependencia de abajo son referencias entre proyectos. Cruzarlas no genera un comentario en la revisión de código: rompe la compilación.

Dominio
La más interna · cero dependencias
Entidades, objetos de valor, eventos de dominio, excepciones de dominio, constantes.
Aplicación
Casos de uso
Depende solo del dominio. Comandos, consultas, handlers, validadores, mappers, behaviors del pipeline.
Infraestructura
Adaptadores
Depende de Aplicación y Dominio. EF Core, Redis, MassTransit, JWT, BCrypt, repositorios.
API
Raíz de composición
Depende de las tres. Controladores, middleware, inyección de dependencias, OpenAPI, mapeo de problem details.

Las capas internas nunca referencian hacia fuera. La dirección se impone donde no admite discusión: en los archivos de proyecto.

Diagrama: cuatro anillos concéntricos — API en el exterior, luego Infraestructura, luego Aplicación, con Dominio en el centro. Las referencias de dependencia apuntan solo hacia dentro; una referencia hacia fuera rompe la compilación.

El pipeline de peticiones.

Cada comando atraviesa los mismos cinco behaviors, del más externo al más interno. Las consultas atraviesan el paso de transacción sin tocarlo.

  1. 01

    Registro

    Nombre del comando y milisegundos transcurridos. Los payloads no se registran nunca.

  2. 02

    Trazas

    Todo el ciclo de vida del handler envuelto en un span de OpenTelemetry.

  3. 03

    Validación

    Se ejecutan todos los validadores registrados; el fallo se convierte en un 400 con un mapa de errores por campo.

  4. 04

    Invalidación de caché

    Las claves de caché se marcan como sucias tras un comando correcto.

  5. 05

    Transacción

    Los comandos se envuelven en una transacción de EF Core. Las consultas se saltan este paso.

Un cliente, una base de datos.

Una cuenta creada a través del registro recibe su propia base de datos SQL, aprovisionada y migrada en el momento en que se crea la cuenta.

La cadena de conexión de esa base de datos se cifra mediante ASP.NET Core Data Protection antes de almacenarse.

Diagrama de cómo se separan los datos de una cuenta de registro: una base de datos dedicada, con las tablas de cada módulo en su propio esquema dentro de ella.

Averiguar a qué cliente pertenece una petición es una cadena de cinco pasos. Se detiene en la primera coincidencia.

  1. 1El claim del token firmadoEl token lleva nuestra firma, que se comprueba antes de confiar en el claim que contiene.
  2. 2La cabecera X-Tenant-IdPara llamadas de servidor a servidor que no llevan token de usuario.
  3. 3Una coincidencia de dominio propioEl dominio del propio cliente, asociado a su cuenta.
  4. 4El subdominio de la plataformaSu nombre en nuestro dominio, cuando no ha traído el suyo.
  5. 5Un parámetro en la cadena de consultaPor nombre: solo en desarrolloEl valor más fácil de la lista para escribir a mano, y por eso va el último.

Trabajo que sobrevive a una caída.

  1. 01

    El trabajo ocurre

    El estado cambia y se guarda. Todavía nada ha tocado la red.

  2. 02

    El aviso se guarda, no se envía

    El mensaje que el resto del sistema necesita se escribe en una tabla de la base de datos, junto al trabajo que lo produjo.

  3. 03

    Un único barredor publica

    Un procesador en segundo plano recorre la tabla y publica lo que encuentra. Nada más en el código habla con el broker.

  4. 04

    Los consumidores esperan repeticiones

    La entrega es al menos una vez. Los consumidores comprueban una clave de evento procesado o se apoyan en un índice único, de modo que un segundo intento falla sin consecuencias.

Los reintentos son finitos. Tras un número fijo de fallos, un mensaje se marca como muerto y deja de reintentarse; preferimos decirlo a dar a entender lo contrario.

Los módulos son invitados en el proceso del host.

Instalar o quitar uno no requiere reinicio ni nuevo despliegue.

Cada módulo se carga en su propio contexto de carga de ensamblados descargable. Instalar uno no reinicia el host, y quitarlo tampoco.

El aislamiento es real, no nominal. Las dependencias propias de un módulo se cargan de forma privada para él. Lo que viene del host es una lista corta y explícita de contratos compartidos — el SDK de módulos, EF Core, MediatR, FluentValidation y las bibliotecas base de ASP.NET y .NET — más los ensamblados de contratos que los módulos publican unos para otros.

La comprobación que nos importa es la que demuestra esto en lugar de afirmarlo: cargar un módulo, soltar todas las referencias a él, forzar una recolección de basura y confirmar después que la referencia débil a su contexto de carga está muerta. Si un módulo no pudiera descargarse de verdad, esa comprobación falla.

La firma es una firma RSA sobre un hash SHA-256, verificada contra una clave pública que instala el operador. Instalar desde una URL exige algo más que una firma válida: el host de origen tiene que estar en una lista de permitidos, y esa lista se entrega vacía, así que la instalación desde URL no hace nada hasta que un operador añade un origen.

Publicación — un evento del host

  • La firma del artefacto — una firma RSA sobre un hash SHA-256 — se verifica contra una clave pública que instala el operador.
  • Los ensamblados se cargan en un contexto de carga privado y descargable.
  • El manifiesto registra el módulo en el marketplace. Sin nuevo despliegue.

Instalación — un evento del tenant

  • Las migraciones se ejecutan en el esquema propio del módulo, dentro de la base de datos de ese cliente.
  • El menú y la lista de permisos se actualizan al instante.
  • Desinstalar apaga el módulo y conserva sus datos: borrar las tablas es una decisión aparte y deliberada.

Un módulo declara lo que es en un único archivo:

{
  "slug": "chat",
  "name": "Chat",
  "version": "1.0.0",
  "sdkVersion": "1.1.0",
  "entryAssembly": "CleverInit.Module.Chat.dll",
  "schema": "chat",
  "dependencies": [],
  "frontend": {
    "remoteEntry": "panel/remoteEntry.json",
    "exposedModule": "./Routes"
  },
  "navSections": [
    { "items": [
      { "label": "Chat", "routerLink": null, "children": [
        { "label": "chat.menu.conversations",
          "routerLink": "/m/chat/conversations" }
      ] }
    ] }
  ],
  "permissions": [
    { "name": "Chat.View", "displayName": "View chat" },
    { "name": "Chat.ManageBots", "displayName": "Manage bots" }
  ],
  "dashboardWidgets": [
    { "key": "chat.unread", "requiredPermission": "Chat.View" }
  ]
}
Recortado de modules/chat/module.json.
El archivo de declaración que indica al host cómo se llama un módulo, qué ensamblado cargar, qué esquema de base de datos le pertenece y qué entradas de menú, permisos y widgets del panel añade.

La suite de pruebas es el contrato.

4.028 pruebas automatizadas se ejecutan sobre la solución host, y las suites de los módulos corren encima, con seguimiento por módulo. La regla es binaria: terminado significa que toda la solución reporta cero fallos.

Aplicación2213
Infraestructura754
Dominio515
Integración de API312
Host de módulos173
SDK de módulos61

Pruebas de la solución host por capa, según el seguimiento del README del repositorio. Las suites de pruebas de los módulos se cuentan aparte.

Dominio95%
Aplicación85%
Infraestructura70%
API70%

Los objetivos de cobertura por capa de la carta técnica, en porcentaje.

  • Una compilación con cualquier aviso es una compilación incompleta. El listón es 0 avisos, 0 errores, en cada commit.
  • Cada módulo incluye una prueba de paridad: un controlador que pide un permiso que su manifiesto no declara rompe la compilación.
  • Todo cambio relevante queda en un historial de auditoría que solo se añade — que solo se añade a través de la aplicación, no sellado criptográficamente frente a alguien con acceso directo a la base de datos.

Fijado, deliberado, aburrido.

Las versiones están fijadas y añadir una biblioteca exige aprobación explícita. Esto es lo que hay hoy en la solución, no una lista de deseos.

Backend

.NET 10 · Clean Architecture · CQRS

  • .NET 10 · C# 13 · ASP.NET Core
  • EF Core 10 · SQL Server
  • MediatR 14 · FluentValidation 12
  • Mapperly 4.3 · Ardalis.Specification 9.3
  • Redis · MassTransit + RabbitMQ 8.5
  • BCrypt · Otp.NET · Asp.Versioning
  • Scalar OpenAPI
  • xUnit · Shouldly · NSubstitute

Panel

Angular 21 · zoneless · signals

  • Angular 21, detección de cambios sin zonas
  • Estado guardado en signals
  • Frontales de módulos como remotes cargados en ejecución
  • Vitest
  • inglés · neerlandés · alemán

Operación

Deliberadamente pequeña

  • Docker · docker-compose para levantar en local
  • Spans de OpenTelemetry en el pipeline de peticiones
  • Registro estructurado: secretos, tokens y datos personales nunca llegan a un destino de logs
  • RFC 7807 en toda la superficie de errores

Los números detrás del acceso.

Un resumen para el cuestionario; la imagen completa está en la página de seguridad.

Tokens de acceso
Caducan en 15 minutos. Los permisos viajan como claims, así que las comprobaciones de autorización son consultas en memoria.
Tokens de refresco
Valores aleatorios de 256 bits, almacenados solo como hash, de un solo uso y rotados en cada refresco.
Contraseñas
BCrypt con factor de trabajo 12, fijado en el código. Bloqueo tras 5 intentos fallidos por defecto, configurable por tenant.
Códigos de un solo uso
Códigos de 6 dígitos por correo o SMS, hasheados en reposo, con caducidad de 5 minutos y un máximo de 3 intentos.

Tres respuestas directas.

  • Hay un portal público de API. Cubre las API públicas de la plataforma — exactamente esas, y nada más. Lo interno se queda interno.
  • Kubernetes es hacia donde va la plataforma. Qué proveedor la ejecutará es algo que deliberadamente no publicamos: ese silencio es una decisión de seguridad, no un descuido.
  • Sin facturación por uso y sin prorrateo: ni hoy, ni previsto. La factura predecible es la función.

¿Quiere profundizar?

Treinta minutos con las personas que lo escribieron. Traiga su pregunta de arquitectura más difícil.

Reservar una llamada