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é
| Origen | Destino | Protocolo | Qué viaja |
|---|---|---|---|
| Navegador del despacho | Aplicación (Fly.io) | HTTPS | Peticiones de página y acciones de servidor. Nada de la aplicación se ejecuta sin sesión y despacho resueltos. |
| Word o LibreOffice | Aplicación (Fly.io) | WebDAV | El 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ón | Clerk | HTTPS | Identidad del usuario y organización activa. Ulpiano no almacena contraseñas. |
| Aplicación | PostgreSQL (Neon) | SQL sobre TLS | Expedientes, plazos, tareas y registro de auditoría. Toda consulta de datos de despacho exige el identificador del despacho en su firma. |
| Aplicación | Cloudflare R2 | S3 sobre TLS | Documentos 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ón | AWS KMS (Aragón) | HTTPS | Llaves 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ón | Resend | HTTPS | Correo 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).