Technik
Wie die Datenhaltung funktioniert
Der übliche Bauplan für so eine Anwendung ist ein Server mit Datenbank und ein Konto je Nutzer. Ich wollte das Gegenteil: Die Daten gehören dem Betrieb und liegen als eine einzige Datei auf dessen Rechner. Dafür muss die Datenbank dorthin, wo die Daten sind — in den Browser.
1. Datei statt Server
Die App besteht nur aus statischem HTML, CSS und JavaScript und wird über GitHub Pages ausgeliefert. Es gibt keinen Server, der Fachdaten sieht. Die Datenbank ist SQLite, als WebAssembly-Modul kompiliert (@sqlite.org/sqlite-wasm), lokal mit der App gebündelt und beim Build gehasht und versioniert wie jede andere Datei. Nichts wird zur Laufzeit von einem fremden CDN nachgeladen — darum funktioniert die Datenbank auch offline.
Ein Detail, das man leicht übersieht: Die Datei auf der Platte des Nutzers ist eine echte SQLite-Datenbank, kein eigenes Exportformat. Sie lässt sich mit jedem SQLite-Werkzeug öffnen, sichern, kopieren und auf einen anderen Rechner mitnehmen.
2. Drei Schichten
Die Datenbank läuft nie im Hauptthread des Browsers. Sie lebt in einem eigenen Web Worker, damit die Oberfläche auch während eines großen Imports bedienbar bleibt. Dazwischen gibt es nur Nachrichten.
Oberfläche · Hauptthread
Astro, TypeScript, Bootstrap
- Formulare, Kalender, Listen, Export (PDF, CSV)
- Zustand und Ereignis-Bus
- Spricht mit der Datenbank nur über Nachrichten
- Hält das Datei-Handle und schreibt die Datei
⇄postMessage
ID, Zeitlimit,
Transfer großer Puffer
Datenbank · Web Worker
SQLite 3.54 als WebAssembly
- Schema mit Migrationen (
user_version 1 bis 11)
- WAL, Fremdschlüssel, 20 MB Seitencache
- Alle Abfragen, seitenweise Listen, BVL-Import
- Export der ganzen Datenbank als Bytes
⇄Bytes
Export · Import
Ablage · beim Nutzer
Eine .sqlite-Datei
- File System Access API, Handle in IndexedDB für den Auto-Start
- OPFS-Modus, wenn der Browser ihn erlaubt
- Fallbacks: JSON-Datei, localStorage, Arbeitsspeicher
3. Speichern, Schritt für Schritt
- Eine Eingabe, etwa eine neue Anwendung, geht als Nachricht an den Worker. Jede Nachricht trägt eine ID und ein Zeitlimit; die Antwort kommt asynchron zurück.
- Der Worker schreibt in seine SQLite-Datenbank. Die liegt im Arbeitsspeicher des Workers oder, wenn der Browser es zulässt, im Origin Private File System (OPFS).
- Danach exportiert der Worker die komplette Datenbank als Bytefolge (
sqlite3_js_db_export) und übergibt den Puffer ohne Kopie an den Hauptthread (Transferable).
- Der Hauptthread schreibt die Bytes über das Datei-Handle in die .sqlite-Datei des Nutzers (
createWritable, write, close). Die Datei auf der Platte ist damit immer eine vollständige, konsistente Datenbank.
- Änderungen kurz hintereinander werden gebündelt: Läuft schon ein Schreibvorgang, merkt sich die App nur, dass danach noch einmal geschrieben werden muss. Ein Schreibvorgang pro Änderungsschub statt einer je Tastendruck.
4. Öffnen: Inhalt statt Dateiendung
Beim Öffnen liest die App die Datei als Bytes und prüft die Signatur am Anfang: SQLite format 3. Der Inhalt entscheidet, nicht die Endung. Eine Datei darf .json heißen und trotzdem eine Binärdatenbank sein — frühere Versionen speicherten als JSON, und genau diese Mischung hat einmal dazu geführt, dass eine intakte Datenbank als „leer“ erschien. Seitdem gilt die Signatur.
Ist es SQLite, macht der Worker die Datei per sqlite3_deserialize direkt zur aktiven Datenbank; im OPFS-Modus importiert OpfsDb.importDb sie in die persistente Datei. Ist es JSON, wird der Bestand als Schnappschuss importiert. Beide Wege enden in derselben Datenbank, und die Migrationen laufen in jedem Fall.
5. Die BVL-Daten: eigene Pipeline
Die amtliche Zulassungsdatenbank des BVL (Bundesamt für Verbraucherschutz und Lebensmittelsicherheit) ist über eine öffentliche API erreichbar, aber nicht für den Browser gemacht: viele Endpunkte, viele Anfragen, kein Offline-Betrieb. Deshalb läuft die Aufbereitung vorab in einer eigenen Pipeline, dem öffentlichen Repository pflanzenschutz-db:
- Alle zwei Tage holt ein GitHub-Actions-Lauf die Daten aller BVL-Endpunkte, prüft sie und reichert sie an — unter anderem mit Bio-Kennzeichnung, Wirkstoffnamen und Gefahrenhinweisen.
- Das Ergebnis ist eine SQLite-Datei, komprimiert mit Brotli und als ZIP, veröffentlicht über GitHub Pages samt Manifest mit Größe und SHA-256-Prüfsumme je Datei.
- Die App lädt das Manifest, wählt das kleinste Format, das der Browser entpacken kann, zeigt den Fortschritt an und übergibt die Bytes an den Worker.
- Der Worker öffnet den Download als temporäre In-Memory-Datenbank und kopiert die Tabellen in einer Transaktion in die Datenbank des Nutzers — in Abhängigkeitsreihenfolge, damit die Fremdschlüssel stimmen.
- Danach steckt der komplette Katalog in der eigenen .sqlite-Datei und ist offline durchsuchbar. Ein Protokoll in der Datenbank hält fest, welcher Stand importiert ist.
| Stand 25.10.2026 | Wert |
| Mittel im Katalog | 2 165 |
| Anwendungen (Mittel je Kultur und Schaderreger) | 30 836 |
| Wirkstoffe | 482 |
| Download, Brotli / ZIP | 6,8 MB / 12,5 MB |
| Entpackt (SQLite) | 113 MB |
6. Offline und als App
Ein Service Worker lädt beim Installieren alle gehashten Build-Dateien vor, das WebAssembly-Modul eingeschlossen. Danach startet die App ohne Netz. Jeder Build stempelt eine neue Cache-Version in den Service Worker; ohne diesen Schritt würden installierte Nutzer auf altem Code hängen bleiben, weil der Browser einen byte-gleichen Service Worker nie neu installiert.
Als installierte Desktop-App registriert sich Digitale PSM über die File Handling API für .sqlite, .db und .json: Ein Doppelklick im Explorer öffnet die Datenbank direkt in der App. Das zuletzt benutzte Datei-Handle liegt in IndexedDB; beim nächsten Start fragt die App nur die Berechtigung ab und öffnet dieselbe Datei wieder.
7. Leistung und Robustheit
- Kein Kopieren großer Puffer. Datenbank-Export und BVL-Import wandern per Transfer zwischen den Threads, nicht als strukturierte Kopie. Bei 100 MB ist das der Unterschied zwischen flüssig und eingefroren.
- Zeitlimits je Aktion. Normale Aufrufe haben 30 Sekunden, der BVL-Import bis zu acht Minuten, weil Browser Timer in Hintergrund-Tabs drosseln. Stürzt der Worker ab, werden alle offenen Aufrufe sofort abgewiesen statt ewig zu hängen.
- Seitenweise statt alles. Historie, Mittel und GPS-Punkte werden mit Cursor seitenweise geladen; die Oberfläche rendert nur die sichtbaren Zeilen.
- Eine geschlossene Datenverlust-Falle. Im SQLite-Modus pflegt der Worker die Historie direkt, und sie wird nicht mehr im Zustands-Schnappschuss mitgeschickt. Sonst hätte das Speichern einer Einstellung die veraltete Liste zurückgeschrieben und alle seit dem Öffnen erfassten Einträge gelöscht.
8. Grenzen, ehrlich
- Desktop zuerst. Die Datei-Anbindung über die File System Access API gibt es nur in Chromium-Browsern am Desktop (Chrome, Edge). Auf dem Handy fehlt sie, in Safari auf dem iPhone genauso wie in Chrome auf Android: Dort lässt sich keine Datei öffnen, und eine neu angelegte Datenbank lebt nur bis zum nächsten Laden der Seite. Eine Handy-Version braucht einen anderen Speicherweg (OPFS oder IndexedDB) und ist noch nicht gebaut.
- OPFS braucht Isolation. Persistenter Speicher im Worker über OPFS setzt Cross-Origin-Isolation voraus (COOP/COEP-Header), die GitHub Pages nicht setzen kann. Im Live-Betrieb läuft die Datenbank deshalb im Arbeitsspeicher des Workers; die Wahrheit liegt in der Datei auf der Platte.
- Ganze Datei statt Delta. Jeder Speichervorgang schreibt die komplette Datei neu. Für die Betriebsdaten selbst ist das unkritisch; mit importiertem BVL-Katalog ist die Datei allerdings über 100 MB groß, und dann kostet jedes Speichern spürbar Zeit. Das ist bewusst einfach gehalten; der nächste Schritt wäre ein Delta auf Seitenebene.
9. Steckbrief
| App | digitale-psm.de — kostenlos, werbefrei, ohne Konto |
| Sprache | Deutsch (die App richtet sich nach dem deutschen Zulassungsrecht und der BVL-Datenbank) |
| Seit | April 2025, aktuell Version 2.0 |
| Lizenz & Code | MIT, Quellcode auf GitHub |
| Stack | Astro 5, TypeScript, Bootstrap 5 (lokal gebündelt), SQLite 3.54 als WebAssembly, Web Worker, Service Worker, Web App Manifest |
| Datenhaltung | SQLite-Datei beim Nutzer über die File System Access API; OPFS, wenn verfügbar; Fallbacks JSON-Datei und localStorage |
| Zulassungsdaten | BVL-API, aufbereitet in pflanzenschutz-db (GitHub Actions, alle zwei Tage), ausgeliefert als Brotli/ZIP mit Manifest und SHA-256 |
| Hosting | Statisch über GitHub Pages, Build und Deploy per GitHub Actions |
| Dazu | st.digitale-psm.de: Statistik-Dashboard zur BVL-Datenbank, SQL-Abfragen direkt im Browser |