Tecnología
Cómo se guardan los datos
El diseño habitual sería un servidor con una base de datos y una cuenta por usuario. Yo quería que los datos quedaran en manos de la empresa, en un único archivo de su ordenador. Para eso, la base de datos tenía que ejecutarse donde están los datos: en el navegador.
1. Un archivo en lugar de un servidor
La aplicación consiste en HTML, CSS y JavaScript estáticos, alojados en GitHub Pages. Ningún servidor recibe los registros. SQLite está compilado como módulo WebAssembly (@sqlite.org/sqlite-wasm) e incluido en la aplicación, con hash y versión asignados durante la compilación, igual que los demás archivos. No se descarga nada de CDN de terceros durante la ejecución; por eso la base de datos también funciona sin conexión.
Un detalle importante: el archivo del usuario es una base de datos SQLite real, no un formato de exportación propio. Se abre con cualquier herramienta SQLite y se puede respaldar, copiar o trasladar a otro equipo.
2. Tres capas
La base de datos no se ejecuta en el hilo principal del navegador, sino en un Web Worker independiente. Así la interfaz sigue respondiendo durante importaciones grandes. Ambos se comunican únicamente mediante mensajes.
Interfaz · hilo principal
Astro, TypeScript, Bootstrap
- Formularios, calendario, listas y exportación PDF/CSV
- Estado y bus de eventos
- Comunicación con la base de datos solo por mensajes
- Mantiene el acceso al archivo y escribe en él
⇄postMessage
ID, tiempo de espera,
transferencia de búferes grandes
Base de datos · Web Worker
SQLite 3.54 mediante WebAssembly
- Esquema con migraciones:
user_version de 1 a 11
- WAL, claves externas y caché de páginas de 20 MB
- Consultas, listas paginadas e importación del BVL
- Exportación de la base completa como bytes
⇄bytes
exportación · importación
Almacenamiento · del usuario
Un archivo .sqlite
- File System Access API; identificador de archivo guardado en IndexedDB para el arranque automático
- Modo OPFS si el navegador lo permite
- Alternativas: archivo JSON, localStorage y memoria
3. Guardado, paso a paso
- Una entrada, por ejemplo un tratamiento nuevo, se envía al worker como mensaje. Cada mensaje lleva un ID y un tiempo límite; la respuesta llega de forma asíncrona.
- El worker escribe en su base SQLite, situada en su memoria o, si el navegador lo permite, en Origin Private File System (OPFS).
- Después exporta la base completa como una secuencia de bytes (
sqlite3_js_db_export) y transfiere el búfer al hilo principal sin copiarlo, mediante Transferable.
- El hilo principal escribe los bytes en el archivo .sqlite del usuario mediante su identificador de acceso:
createWritable, write y close. El archivo del disco contiene así una base completa y coherente.
- Los cambios consecutivos se agrupan. Si ya hay una escritura en curso, la aplicación anota que debe repetirla al terminar. Así se escribe una vez por cada grupo de cambios, no por cada pulsación.
4. Abrir según el contenido del archivo
Al abrir un archivo, la aplicación lee sus bytes y comprueba la firma inicial: SQLite format 3. Decide el contenido, no la extensión. Un archivo llamado .json puede contener una base binaria. Las versiones antiguas guardaban JSON y esa mezcla hizo que una base intacta pareciera «vacía». Desde entonces, la firma determina el formato.
Si es SQLite, el worker convierte el archivo en la base activa con sqlite3_deserialize. En modo OPFS, OpfsDb.importDb lo importa al archivo persistente. Si es JSON, se importa como una instantánea de los datos. Ambas rutas terminan en la misma base y ejecutan las migraciones.
5. Preparación de los datos del BVL
La base oficial de autorizaciones del BVL, la Oficina Federal alemana de Protección del Consumidor y Seguridad Alimentaria, está disponible mediante una API pública. Sin embargo, tiene numerosos endpoints, requiere muchas peticiones y no permite trabajar sin conexión. Por eso preparo los datos por adelantado en un proceso independiente, en el repositorio público pflanzenschutz-db:
- Cada dos días, GitHub Actions descarga los datos de todos los endpoints del BVL, los valida y añade información, como indicadores de uso en agricultura ecológica, nombres de sustancias activas e indicaciones de peligro.
- El resultado es un archivo SQLite, comprimido con Brotli y ZIP, publicado en GitHub Pages junto con un manifiesto que indica el tamaño y la suma SHA-256 de cada archivo.
- La aplicación descarga el manifiesto, elige el formato más pequeño que el navegador pueda descomprimir, muestra el progreso y entrega los bytes al worker.
- El worker abre la descarga como una base temporal en memoria y copia sus tablas a la base del usuario dentro de una única transacción. Respeta el orden de dependencias para mantener las claves externas.
- El catálogo completo queda en el archivo .sqlite del usuario y se puede consultar sin conexión. Un registro dentro de la base indica qué versión se importó.
| Datos del 25 de octubre de 2026 | Valor |
| Productos del catálogo | 2,165 |
| Usos autorizados (producto por cultivo y plaga) | 30,836 |
| Sustancias activas | 482 |
| Descarga, Brotli / ZIP | 6.8 MB / 12.5 MB |
| Sin comprimir, SQLite | 113 MB |
6. Sin conexión y como aplicación
Al instalarse, un service worker guarda en caché todos los archivos de compilación con hash, incluido el módulo WebAssembly. Después, la aplicación arranca sin red. Cada compilación incorpora una nueva versión de caché al service worker. Sin ese cambio, los usuarios con la aplicación instalada conservarían el código antiguo, porque el navegador no reinstala un service worker idéntico byte por byte.
Como aplicación de escritorio instalada, Digitale PSM se registra mediante File Handling API para .sqlite, .db y .json. Un doble clic en el explorador abre directamente la base. El identificador del último archivo se guarda en IndexedDB; al volver a arrancar, la aplicación solo pide permiso y abre el mismo archivo.
7. Rendimiento y robustez
- Sin copiar búferes grandes. Las exportaciones de la base y la importación del BVL se transfieren entre hilos, en lugar de usar clonación estructurada. Con 100 MB, esto puede marcar la diferencia entre una interfaz fluida y una bloqueada.
- Tiempos límite por operación. Las llamadas habituales tienen 30 segundos; la importación del BVL, hasta ocho minutos, porque los navegadores ralentizan los temporizadores en pestañas de fondo. Si el worker falla, todas las llamadas pendientes se rechazan de inmediato, sin quedar esperando indefinidamente.
- Carga por páginas. El historial, los productos y los puntos GPS se cargan por páginas con un cursor. La interfaz solo representa las filas visibles.
- Una causa de pérdida de datos corregida. En modo SQLite, el worker gestiona el historial directamente y ya no lo incluye en la instantánea del estado. De lo contrario, guardar un ajuste podía sobrescribirlo con una lista desactualizada y borrar todas las entradas añadidas desde que se abrió el archivo.
8. Limitaciones actuales
- Pensada primero para escritorio. El acceso mediante File System Access API solo está disponible en navegadores Chromium de escritorio, como Chrome y Edge. No está presente en teléfonos, ni en Safari para iPhone ni en Chrome para Android. En esta versión no se pueden abrir archivos allí, y una base nueva solo dura hasta recargar la página. La versión móvil necesita otro método de almacenamiento, como OPFS o IndexedDB, y todavía no está desarrollada.
- OPFS requiere aislamiento. El almacenamiento persistente dentro del worker mediante OPFS exige aislamiento entre orígenes, con cabeceras COOP/COEP, que GitHub Pages no permite configurar. Por eso, en producción la base se ejecuta en la memoria del worker y la copia de referencia está en el archivo del disco.
- Se reescribe el archivo completo. Cada guardado vuelve a escribir todos los datos. Para los registros de la empresa no supone un problema, pero con el catálogo BVL importado el archivo supera los 100 MB y el tiempo de guardado se nota. Se ha mantenido este diseño sencillo deliberadamente; el siguiente paso sería escribir solo las páginas modificadas.
9. Ficha técnica
| Aplicación | digitale-psm.de — gratuita, sin publicidad ni cuenta |
| Idioma | Alemán; la aplicación sigue la normativa alemana de autorizaciones y la base del BVL |
| Inicio | Abril de 2025; versión actual 2.0 |
| Licencia y código | MIT, código fuente en GitHub |
| Tecnologías | Astro 5, TypeScript, Bootstrap 5 incluido localmente, SQLite 3.54 mediante WebAssembly, Web Worker, service worker y manifiesto de aplicación web |
| Almacenamiento | Archivo SQLite del usuario mediante File System Access API; OPFS cuando está disponible; alternativas con archivo JSON y localStorage |
| Datos de autorizaciones | API del BVL, preparación en pflanzenschutz-db mediante GitHub Actions cada dos días, distribución en Brotli/ZIP con manifiesto y SHA-256 |
| Alojamiento | Archivos estáticos en GitHub Pages; compilación y despliegue con GitHub Actions |
| Proyecto relacionado | st.digitale-psm.de: panel de estadísticas de la base del BVL, con consultas SQL directamente en el navegador |