Cómo está construido

Arquitectura

Ulpiano guarda expedientes ajenos. Quien va a confiárselos tiene derecho a saber de qué está hecho, dónde se ejecuta cada pieza y qué viaja entre ellas. Lo que ya funciona está aquí; lo que solo está decidido, en su propio apartado al final.

El mapa

Qué habla con qué

Todo lo que el despacho toca entra por la aplicación. Ningún servicio externo habla directamente con el navegador, y el almacén de documentos solo recibe documentos ya cifrados.
Conexiones entre los sistemas de Ulpiano, con el protocolo empleado y los datos que circulan por cada una.
OrigenDestinoProtocoloQué viaja
Navegador del despachoAplicación (Fly.io)HTTPSPeticiones de página y acciones de servidor. Nada de la aplicación se ejecuta sin sesión y despacho resueltos.
Word o LibreOfficeAplicación (Fly.io)WebDAVEl escrito se abre en el editor de siempre y se guarda contra el expediente. La credencial es un identificador de un solo uso, no la sesión del navegador.
AplicaciónClerkHTTPSIdentidad del usuario y organización activa. Ulpiano no almacena contraseñas.
AplicaciónPostgreSQL (Neon)SQL sobre TLSExpedientes, plazos, tareas y registro de auditoría. Toda consulta de datos de despacho exige el identificador del despacho en su firma.
AplicaciónCloudflare R2S3 sobre TLSDocumentos y sus versiones, cifrados antes de salir de la aplicación. El navegador no se conecta al almacén: sube y descarga a través de la aplicación, y el bucket no es público.
AplicaciónAWS KMS (Aragón)HTTPSLlaves de despacho cerradas, para generarlas en el alta y abrirlas al leer o guardar un documento, junto con el identificador del despacho. Ningún documento ni dato de los expedientes.
AplicaciónResendHTTPSCorreo transaccional. Sale por un único punto del código, para que ningún envío escape al registro.

La pila

Cada capa y por qué esa y no otra

LenguajeTypeScript, de la base de datos a la pantalla
Un solo lenguaje y un solo juego de tipos. El esquema de la base de datos es la fuente de los tipos que llegan al componente.
AplicaciónNext.js 16 (App Router) y React 19
Renderiza en el servidor por omisión. La pantalla se compone junto a la base de datos, y al navegador solo baja el JavaScript de los componentes que necesitan interacción.
Base de datosPostgreSQL gestionado por Neon, región de Fráncfort
Postgres por previsibilidad. Neon aporta recuperación a un instante anterior y una copia de la base por rama de desarrollo, que es lo que permite probar una migración sin tocar la de producción.
Acceso a datosDrizzle
Está cerca del SQL a propósito. La consulta que se escribe en el repositorio se parece a la que recibe Postgres, y se lee entera sin ejecutarla.
IdentidadClerk
Sesiones y verificación en dos pasos son un problema resuelto que no compensa reimplementar. Cada despacho es una organización de Clerk, y de ahí cuelga el aislamiento.
DocumentosCloudflare R2, almacenamiento de objetos compatible con S3
Los ficheros no viven en la base de datos ni en el disco del servidor. Ulpiano los versiona y los guarda cifrados, así que en R2 solo hay bytes ilegibles.
Cifrado de documentosAES-256-GCM con la biblioteca estándar de Node, llave raíz en AWS KMS (Aragón)
Sigue una construcción publicada para cifrar por tramos (STREAM), sin bibliotecas de terceros, de modo que un documento se descifra según llega sin cargarlo entero en memoria. La llave raíz está en un proveedor distinto del que guarda los documentos.
Generacióndocxtemplater sobre plantillas .docx del propio despacho
La plantilla es un documento de Word que el despacho reconoce y puede editar, no un formato propietario.
InterfazTailwind y componentes propios sobre Base UI
Base UI resuelve accesibilidad y comportamiento; el aspecto es código de este repositorio, no un tema de terceros.
AlojamientoFly.io, contenedores en Fráncfort (Unión Europea)
Nadie administra servidores ni aplica parches de sistema. Es la condición para que una sola persona sostenga el producto.

Decisiones

Las que más condicionan el resto

El escrito se edita en Word, no en una imitación de Word

Un editor de textos dentro del navegador nunca alcanza al que el despacho ya domina, y romper el formato de un escrito el día de su presentación no es un defecto asumible. Ulpiano sirve el documento por WebDAV. Se abre en Word o LibreOffice, se guarda con normalidad, y cada guardado vuelve al expediente como una versión nueva. El editor cree estar guardando en una carpeta de red; en realidad guarda en el expediente.

El cómputo de plazos tiene su sitio reservado, y el sitio está vacío

Días hábiles, agosto inhábil, festivos del partido judicial, el día de gracia del artículo 135 de la Ley de Enjuiciamiento Civil. Un error ahí no es un fallo de programa, es la responsabilidad profesional de quien confió en él. Por eso el cálculo tiene desde el principio un paquete propio, packages/plazos, y una regla alrededor: nadie suma días fuera de él. Y por eso ese paquete sigue sin una sola línea de cálculo, en vez de una primera versión razonable. Se construirá con sus casos límite documentados en pruebas o no se construirá.

Mientras tanto, Ulpiano no calcula ningún plazo. Registra el vencimiento que anota el despacho y lo presenta ordenado por proximidad. Quien necesite hoy el cómputo automático no lo va a encontrar aquí.

La separación entre despachos la aplica la base de datos

Lo habitual es que cada consulta lleve escrito «y que sea de este despacho», de modo que la separación entre clientes depende de que nadie olvide esa línea. Ulpiano no depende de eso. La aplicación se conecta con un usuario que no es dueño de las tablas, y cada tabla con datos de despachos tiene una regla que aplica la propia base: si la consulta no identifica el despacho, devuelve cero resultados en lugar de los datos de otro.

Así, cuando alguien escribe una consulta nueva y olvida el filtro, la pantalla sale vacía. Es un fallo visible, que se corrige, en lugar de una fuga silenciosa.

Cada documento se guarda cifrado con su propia llave

Antes de enviar un documento al almacén, la aplicación lo cifra con una llave que solo sirve para ese documento. Esa llave se guarda cerrada con la llave del despacho, y la del despacho se guarda cerrada con una llave raíz. La llave raíz no sale nunca del servicio de llaves de Amazon Web Services (AWS KMS) en su región de Aragón. Solo se le puede pedir que genere, cierre o abra otras llaves.

Las tres piezas están en proveedores distintos. Los documentos, en Cloudflare R2. Las llaves de despacho cerradas, en la base de datos de Neon. La llave raíz, en AWS. Para leer un documento hacen falta los tres, y quien consiga entrar en uno o en dos solo obtiene datos ilegibles. Cada documento además va atado a su sitio: copiado a la ruta de otro documento, no se puede abrir.

Cada vez que la aplicación pide a AWS que abra la llave de un despacho, AWS lo anota en un registro que se guarda seis años en un almacén bloqueado. Durante ese plazo nadie puede modificarlo ni borrarlo, tampoco el titular de la cuenta. La llave abierta se conserva cinco minutos en la memoria del servidor, así que ese registro dice cuándo se abrió la llave de un despacho. Qué documento abrió cada persona lo dice el registro de auditoría de Ulpiano.

El usuario de mantenimiento no puede leer expedientes

Hay un segundo usuario de base de datos, el del mantenimiento de la plataforma, y sus permisos alcanzan a cuatro cosas: qué despachos existen, qué plan tienen, qué ha dicho la pasarela de pago y poco más. Sobre expedientes, documentos, clientes o el registro de auditoría no tiene permiso de lectura, así que la base de datos rechaza esas consultas.

El registro de auditoría no se puede borrar desde el programa

Se anota lo que se cambia y también lo que se consulta: abrir un expediente, una ficha de cliente o un documento deja constancia de quién, cuándo y sobre qué, y abrir el escrito en Word cuenta igual. El registro guarda referencias, no contenido: dentro no hay nombres de partes ni texto de escritos, y lo que la pantalla muestra se compone al consultarlo.

El usuario con el que Ulpiano se conecta puede añadir entradas, pero no modificarlas ni borrarlas, porque la base de datos no le concede ese permiso. Lo consulta la administración del despacho, y sirve para responder con precisión cuando un cliente pregunta quién ha tocado su asunto.

Administrar la plataforma y usarla son dos programas distintos

Las altas, el soporte y la facturación van en una aplicación distinta, con su propio despliegue y su propia autenticación, y no en una sección oculta de la que usan los despachos. Hoy apps/admin está vacía. Ni esas funciones ni esa autenticación existen todavía. La frontera se traza antes de construir nada detrás, porque moverla con expedientes ya dentro sería otro problema. Unirlas habría significado que un fallo de permisos en el panel de soporte alcanzara expedientes de clientes.

Residencia

Dónde viven los datos

La base de datos, las funciones de la aplicación y el almacén de documentos residen en regiones de la Unión Europea, y la llave raíz del cifrado, en España. Los proveedores concretos, su función y las garantías aplicables a cada uno están enumerados en la política de privacidad y en el encargo de tratamiento, que es el documento que firma el despacho como responsable.

El registro de auditoría anota cada alta, modificación y borrado, y también cada descarga o previsualización de un documento: quién, cuándo y sobre qué. Es un registro que solo crece. Ninguna operación del programa modifica ni elimina una entrada ya escrita. Se conserva seis años. El borrado de datos de negocio es siempre lógico; la eliminación física solo ocurre en el proceso de baja del despacho, junto con la entrega de la exportación completa.

Lo que falta

Decidido, todavía no construido

Estas piezas tienen decisión tomada y sitio reservado en la arquitectura, pero no hay una línea de código en producción que las implemente. Aparecen aquí para que nadie las dé por hechas.

Cómputo de plazos procesalesPrevisto
packages/plazos existe vacío, con su regla ya escrita: nadie suma días fuera de él. Falta el cálculo entero. Días hábiles, agosto inhábil, festivos por partido judicial, artículo 135 LEC.
LexNETPrevisto
Pendiente de homologación con el Ministerio. En el código hay una interfaz definida y ninguna implementación detrás.
Firma con certificado del abogado (Autofirma)Previsto
Requisito estructural de LexNET, porque la clave privada no sale del equipo del letrado. Queda atado a su calendario.
Firma de clientes (Signaturit)Previsto
Prestador cualificado eIDAS elegido; integración no comenzada.
Conversión a PDF (Gotenberg)Previsto
El contenedor ya corre en Fly.io, región fra, sin IP pública. Todavía no lo llama nadie, y la generación produce .docx.
Trabajos en segundo plano (Inngest)Previsto
Elegido para los flujos largos y con reintentos que traerá LexNET. Todavía no hay ninguno que lo justifique.
Plano de administración (apps/admin)Previsto
El proyecto existe separado y ya se despliega aparte, todavía vacío. Su usuario de base de datos sí existe, y solo alcanza a metadatos: no tiene permiso sobre expedientes. Las altas de despacho, el soporte y la facturación se construirán ahí; hoy se atienden a mano.
Facturación del despacho a sus clientesPrevisto
Aplazada a propósito. Existe el resumen económico del expediente; el resto llegará cuando lo pidan los despachos.

Cada decisión de arquitectura se escribe antes de implementarla, con las alternativas que se descartaron y el motivo. Puede leerlas en el blog del proyecto (se abre en una ventana nueva).