Arquitectura
Cómo encajan el gateway, el frontend, el backend, la base de datos y el almacén de objetos, y cómo se protegen los certificados y documentos.
Garabato es un monorepo con dos apps detrás de un gateway Caddy, más PostgreSQL y un almacén de objetos compatible con S3. Solo el gateway publica un puerto, así que el navegador ve un único origen y nunca habla directamente con el backend, la base de datos ni el almacén.
| Servicio | Stack | Puerto | Papel |
|---|---|---|---|
| Gateway | Caddy | 80 (publicado como FRONTEND_PORT) |
Punto de entrada HTTP público. El único sitio que reparte rutas entre apps. |
| Frontend | Astro SSR, React | 4321 (interno) |
La interfaz web. |
| Backend | Bun, Hono, oRPC, Better Auth, Drizzle | 3000 (interno) |
Autenticación, la API y la firma. Es el dueño de la base de datos y del almacén. |
| PostgreSQL | PostgreSQL 17 | 5432 (interno) |
Usuarios, certificados, la biblioteca, las versiones, los registros de firma y las trazas. |
| MinIO | Compatible con S3 | 9000 (solo interno, no publica nada) |
Los PDF cifrados. Opcional: vale cualquier almacén externo compatible con S3. |
| Loki | Grafana Loki | 3100 (interno) |
Registro de actividad opcional, consultable en /admin/logs. |
Recorrido de las peticiones
El gateway manda /rpc, /api, /scalar y /openapi.json al backend y cualquier otra ruta al frontend. /health responde ok para el healthcheck. Como la web y la API comparten origen, el navegador nunca hace llamadas entre orígenes y las cookies de sesión funcionan sin más.
Al arrancar, el backend aplica las migraciones de la base de datos y crea el administrador del .env.
Anatomía de una firma
- Pulsas firmar en el visor. El frontend llama al backend por oRPC con la versión del documento que estás viendo, el certificado, dónde va el sello y el motivo y el lugar opcionales.
- El backend comprueba tu sesión y que tanto el documento como el certificado son tuyos.
- Descifra el certificado (con la contraseña que escribiste o la recordada) y la versión actual del PDF.
- Dibuja el sello visible, si lo hay, y firma el PDF: una firma CAdES separada sobre SHA-256, con el atributo ESS signing-certificate-v2 y la cadena del emisor incluida, con
SubFilter ETSI.CAdES.detached(PAdES básica B-B). SiTSA_URLestá definida, pide a esa autoridad de sellado de tiempo un sello RFC 3161 sobre la firma y lo incrusta (B-T); si la TSA no lo da, la firma falla en lugar de guardarse como B-B. - El PDF firmado se cifra y se guarda como una nueva versión, y se añade un registro al historial de firmas.
Modelo de seguridad
- Cifrado de sobre. Cada certificado y cada documento tiene su propia clave de datos aleatoria, envuelta con la clave maestra
CERTIFICATE_ENCRYPTION_KEY. Los valores (el fichero PKCS#12, una contraseña recordada, cada versión de un documento) se sellan con AES-256-GCM, ligados al registro y al hueco al que pertenecen, así que un valor solo se abre donde se selló. - El almacén solo guarda texto cifrado. Los PDF se cifran antes de escribirse en el almacén de objetos, y el almacén nunca sirve archivos directamente: toda descarga pasa por la API, que la descifra.
- Las contraseñas de los certificados no se guardan, salvo que pidas a la app que recuerde una, y entonces se cifra igual.
- Todo es por usuario. Los documentos y certificados pertenecen al usuario con sesión; los de otro usuario responden como no encontrados.
- Las sesiones son cookies
httpOnly,SecureySameSite=Lax./rpcnunca aceptaGETy/apirechaza las navegaciones de nivel superior desde otra web, lo que deja fuera el CSRF sin ningún plugin en el cliente.
La API
La API está versionada bajo /rpc/v1 (oRPC, la que usa el frontend) y /api/v1 (OpenAPI, para scripts y otros servicios). La referencia interactiva está en /scalar y la especificación en /openapi.json, las dos solo para administradores.
Desde scripts, autentícate con una API key enviada en la cabecera x-api-key (consulta Usuarios e inicio de sesión):
curl -H "x-api-key: $GARABATO_KEY" https://firma.example.com/api/v1/document/list