Umsetzung

Mit dem Outline des Projektes und dem Entscheid darüber, welche Tools verwendet werden, konnte mit der Implementierung gestartet werden.

Projektphasen

Das Backend wurde in Phasen aufgebaut. Jede Phase hatte ein klares Ziel, an dem sich prüfen liess, ob sie abgeschlossen ist.

Phase Inhalt Abgeschlossen
0. Vorbereitung Speicherplatz freigeben, Firewall (ufw) einschalten, DNS-Einträge 2026-09-23
1. Health Check Express App mit GET /v1/health, pm2, nginx für beide Hostnamen, HTTPS-Zertifikat 2026-09-23
2. Postgres PostgreSQL 18 und PostGIS installieren, Rollen, erste Migrationen mit dem Schema 2026-09-24
3. API und Import Lese-API für alle Einträge, Karte als GeoJSON, Netzwerk, Import aus YAML-Dateien 2026-09-24
4. Backups nächtliche Sicherung mit Prüfung und verschlüsselter Kopie ausserhalb des Servers 2026-09-28
5. CMS Admin Panel zum Erfassen und Bearbeiten 2026-09-28

Das Frontend folgte ab dem 2026-10-03.

Aufsetzen des Backends

Da das Frontend abhängig von den Schnittstellen des Backends ist, wurde zuerst das Backend aufgesetzt. Der wichtigste Grund dafür ist, dass das Backend Dreh- und Angelpunkt des gesamten Projekts ist. In das Backend werden die Daten gespiesen, die dargestellt werden, und vom Backend werden die Daten für das Frontend bezogen. Das bedeutet, es müssen die entsprechenden Endpunkte eingerichtet und danach überprüft werden, ob diese erreichbar sind. Zwar könnte auch mit dem Frontend gestartet und mit einem Testsatz an Daten dessen Funktionalität geprüft werden, doch dann wäre noch nicht geprüft, ob das Frontend im tatsächlichen Fall das Backend ansteuern kann. Weiter muss Postgres auf dem Backend eingerichtet werden, welches als zentrales System für die Funktionalität der geplanten Website unabdingbar ist.

Diese Projekt folgt der selben Ordnerstruktur auf dem Server, wie meine anderen Projekte. Der nächste freie Port des Servers war 3004, auf diesem läuft das Projekt. Damit der Code und alle Veränderungen an diesem nachvollziehbar und reversibel bleiben, wird der gesamte Code auf GitHub gespeichert, im selben Repository, in welchem sich auch der Code des Frontends befindet. Zugangsdaten (Credentials) werden dagegen nie auf GitHub gespeichert (siehe Vertrauliche Daten "Vertrauliche Daten"). Für den Zugriff vom Server auf GitHub wird ein Deploy Key verwendet, der in GitHub nativ eingerichtet werden kann und nur Zugriff auf dieses eine Repository erlaubt.

Ubuntu

Der Server läuft mit Ubuntu 22.04 LTS (Long Term Support). Der Standard-Support für diese Version endet im April 2027. Damit ist das Projekt auf dieser Ubuntu-Version noch machbar. Die Ubuntu Version ist danach immernoch gut nutzbar, es soll aber ab April 2027 eine Migration auf Ubuntu 24.04 LTS in Erwägung gezogen werden.

Node.js

Node.js ist eine Open-Source-JavaScript-Laufzeitumgebung (Runtime). Sie ermöglicht es, JavaScript ausserhalb des Browsers auszuführen, in diesem Fall auf dem Server. Einfacher gesagt: Node.js führt den Code der Backend-App aus.

npm

npm (Node Package Manager) ist ein Paketmanager und eine Software-Registry für Node.js und die JavaScript-Entwicklung. npm wird zusammen mit Node.js installiert und lädt die benötigten Libraries herunter, zum Beispiel mit npm install express.

PostgreSQL

Für das Projekt wird PostgreSQL in der Version 18 verwendet, der aktuellen stabilen Version. Installiert wurde sie aus dem offiziellen Repository von PostgreSQL und nicht aus dem von Ubuntu, weil Ubuntus Version 14 im November 2026 keine Sicherheitsupdates mehr erhält. Version 19 stand beim Aufsetzen kurz vor der Veröffentlichung (Stand 2026-09-24 war sie in der Beta 4 verfügbar). Version 18 wird aber noch jahrelang unterstützt und ist mit der aktuellen PostGIS-Version kompatibel.

PostGIS

PostGIS ist eine Erweiterung für PostgreSQL, die geografische Daten wie Koordinaten und Regionen speichern und abfragen kann. Verwendet wird PostGIS 3.6.x in der jeweils aktuellsten Patch-Version.

certbot / Let's Encrypt

Let's Encrypt stellt kostenlose SSL-Zertifikate aus, die für HTTPS benötigt werden. certbot ist das Programm, das diese Zertifikate beantragt, in nginx einrichtet und automatisch erneuert.

Libraries der Node.js-App

Zu Beginn war eine längere Liste von Libraries geplant: express-session und connect-pg-simple für die Sessions, express-rate-limit gegen das Durchprobieren von Passwörtern, bcrypt für die Passwörter, ejs für die HTML-Seiten und joi für die Prüfung der Eingaben. Bei der Umsetzung wurden diese Aufgaben jedoch mit Bordmitteln von Node.js oder mit wenigen Zeilen eigenem Code gelöst. Jede zusätzliche Library ist Code, den man nicht selbst kontrolliert und der aktuell gehalten werden muss. Die folgenden Libraries werden tatsächlich verwendet. Jede davon ist ein eigenständiges npm-Paket.

Was stattdessen ohne Library gelöst ist:

Sicherheit

CORS (Cross-Origin Resource Sharing)

Ein Browser hat die Sicherheitsregel der "Same-Origin Policy". Sie besagt, dass JavaScript auf einer Website nur Antworten von derselben Herkunft (Protokoll, Domain und Port) lesen darf. Da die API unter einer anderen Herkunft läuft als die eigentliche Website (api.arthistory.piogino.ch statt arthistory.piogino.ch), verwende ich CORS. CORS ist ein Mechanismus, mit dem der Server eine Ausnahme von dieser Sicherheitsregel erteilen kann: Er schickt einen Header mit, der festlegt, welche Domain seine Antworten lesen darf, und der Browser kontrolliert diese Erlaubnis. Erlaubt wird nur die eigene Domain https://arthistory.piogino.ch, keine beliebigen Websites. Die CORS-Header werden nur von der App gesetzt und nicht zusätzlich in nginx, weil doppelte Header dazu führen, dass der Browser die Antwort ablehnt.

nginx und Firewall

nginx steht vor der Node.js-App. Er nimmt die Anfragen aus dem Internet entgegen, kümmert sich um HTTPS und leitet die Anfragen an die App weiter, die selbst nicht direkt aus dem Internet erreichbar ist (sie hört nur auf 127.0.0.1:3004). Anhand des Hostnamens unterscheidet die App zwischen api.arthistory.piogino.ch und admin.arthistory.piogino.ch, die beide zur selben App führen. Die API ist nur unter api.… erreichbar, das CMS nur unter admin.…. Auf dem Server sind von aussen nur drei Ports offen: SSH, HTTP (80) und HTTPS (443). Alle Apps, auch meine anderen Projekte, sind nur über nginx erreichbar.

Vertrauliche Daten

Vertrauliche Daten, wie beispielsweise die Logindaten für das CMS oder die Passwörter der Datenbank, werden in .env-Dateien ausserhalb des Projektordners gespeichert (~/.config/arthistory/), die nur für den Benutzer des Servers lesbar sind. Damit können sie gar nicht erst mit GitHub synchronisiert werden. Das ist wichtig, weil das Repository öffentlich ist. Zusätzlich prüft vor jedem Commit und vor jedem Push ein Script, ob etwas wie ein Passwort, ein Schlüssel oder ein echter Wert aus den Zugangsdaten hochgeladen werden soll, und blockiert es in diesem Fall. Diese Prüfung hat einige Male angeschlagen, jeweils fälschlicherweise wegen eines Variablennamens wie token oder password. In diesen Fällen wurde die Variable umbenannt, statt die Prüfung zu umgehen. Gelangt ein Passwort doch einmal nach aussen, wird es sofort ersetzt; ein Löschen allein genügt nicht, da es bereits kopiert worden sein könnte.

Login und Sicherheit des CMS

Zuerst wurde ein zweistufiges Login ausprobiert: Das erste Login kam von nginx (ein Passwortdialog des Browsers), das zweite von der App selbst. Dies wurde aber schnell verworfen, weil das zweistufige Login eine unnötige Hürde in der aktuellen Entwicklungsstufe darstellt. Die Aufgabe des ersten Logins, Angreifer fernzuhalten, übernimmt nun fail2ban: Nach 5 fehlgeschlagenen Logins innerhalb von 10 Minuten wird die IP-Adresse für eine Stunde an der Firewall gesperrt. Die eigene Sperre der App (10 Versuche in 15 Minuten) bleibt als zweite Bremse bestehen. Sollte das Projekt tatsächlich längerfristig genutzt werden, kann die Sicherheit des Logins nochmals evaluiert werden, etwa mit einer Zwei-Faktor-Authentifizierung. Weitere Massnahmen: Das Session-Cookie gilt nur für die Admin-Seite und wird bei Anfragen von anderen Websites nicht mitgeschickt. Jede Änderung muss nachweislich von der Admin-Seite selbst kommen. Die Seiten des CMS dürfen nur Scripts vom eigenen Server ausführen.

Beim Einrichten gab es einen Zwischenfall: Bei einem Test des ersten Logins wurde das Passwort des nginx-Dialogs im Protokoll der AI-Sitzung sichtbar. Es wurde sofort ersetzt, und der Vorfall ist im Changelog des Servers dokumentiert. Das eigentliche Login-Passwort der App war nie betroffen.

Kurz nach dem Aufschalten liess das CMS kein Login mehr zu. Die Ursache war eine Sicherheitseinstellung: helmet wies den Browser an, keine Herkunftsangaben mitzuschicken, und unter dieser Einstellung meldete der Browser beim Absenden des Login-Formulars die Herkunft "null". Die Prüfung, dass jede Änderung von der Admin-Seite selbst kommt, lehnte das Login deshalb ab. In den Tests war das nicht aufgefallen, weil das dafür verwendete Programm (curl) die Herkunft so mitschickt, wie man es ihm vorgibt, ein echter Browser aber nicht. Seither gibt der Browser die Herkunft innerhalb der Admin-Seite an, gegenüber anderen Websites weiterhin nicht.

Self-Unban bei zu vielen fehlgeschlagenen Login-Versuchen

sudo fail2ban-client set arthistory-admin unbanip <your IP> fail2ban bringt unter Ubuntu zudem eine Sperre für SSH mit. Diese habe ich bewusst ausgeschaltet: Ein SSH-Programm, das beim Anmelden mehrere Schlüssel nacheinander durchprobiert, könnte mich sonst selbst vom Server aussperren.

Arbeitsablauf und Tests

Der Server ist zugleich der Ort, an dem entwickelt wird, und der Ort, an dem die Website läuft. Damit dabei nichts kaputtgeht, gelten feste Regeln:

Anekdote: Wenn Tests von echten Daten abhängen

Bei der Veröffentlichung der Zeitleiste der Stilepochen fiel auf, dass die öffentliche Website seit dem Vormittag nicht mehr aktualisiert worden war. Vor jeder Veröffentlichung läuft eine Reihe automatischer Tests, und einige davon prüfen die Website mit den echten Daten der Datenbank. Zwei Änderungen an den Inhalten hatten diese Tests veralten lassen: Beim Abgleich mit Wikidata kam hinzu, dass Niklaus Weckmann in Ulm gearbeitet hat, weshalb die Karte den Entstehungsort des Heiligen Sebastian nun in Ulm zeigt. Und nach dem Import aus dem Kunsthaus Zürich hatten mehr Werke eine Provenienz, wodurch der Heilige Sebastian nicht mehr unter den ersten zwölf Namen der Legende auf der Startkarte stand. Die Website selbst funktionierte richtig, nur die Tests erwarteten die alten Inhalte. Nach der Anpassung der Tests lief die Veröffentlichung wieder durch. Das zeigt, dass Tests, die mit echten Inhalten arbeiten, mit jeder neuen Erfassung in der Datenbank brechen können. Dasselbe passierte am 2026-10-19 (ein Test erwartete den Link auf das Metropolitan Museum noch ohne die neue öffentliche Nummer in der Adresse) und am 2026-10-22, als neue Quellen der Grossen Welle und neue Einträge vier Tests brechen liessen. Die Tests prüfen seither, wo möglich, das Verhalten der Website statt bestimmter Inhalte.

Server Backup

Die Datenbank wird jede Nacht automatisch gesichert. Jede Sicherung wird zur Kontrolle in eine leere Datenbank zurückgespielt und mit der laufenden verglichen; erst dann gilt sie als gültig. Die Sicherungen der letzten 14 Tage bleiben auf dem Server. Hat sich der Inhalt seit der letzten Sicherung geändert, wird zusätzlich eine verschlüsselte Kopie (gpg, AES256) in ein privates GitHub Repository hochgeladen. Damit überstehen die Daten auch einen Verlust des ganzen Servers. Die Passphrase für die Verschlüsselung liegt auf dem Server und in meinem Passwort-Manager. Vor grösseren Änderungen an der Datenbankstruktur wurde zudem jeweils von Hand eine zusätzliche Sicherung erstellt.

Health Test

Um zu testen, ob die API funktionsfähig ist, kann die folgende URL aufgerufen werden: API health check oder api.arthistory.piogino.ch/v1/health. Auch ein Reboot führt nicht zu einem Abbruch des Service (pm2 ist korrekt eingerichtet).£