PostgreSQL
WorkOffice Pro speichert alles Geschäftliche in PostgreSQL 16: Betriebe, Kunden, Belege, Zahlungen, Bankumsätze, das Protokoll. Die Datenbank läuft als eigener Container workoffice-postgres und hat keinen Port nach außen – nur die API im selben Netz spricht mit ihr. Dateien wie Belegbilder und archivierte PDFs liegen nicht in der Datenbank, sondern in der Dateiablage (Volume ablage).
Zwei Zugänge, und das ist der Kern
| Benutzer | aus der .env | Wofür |
|---|---|---|
workoffice | POSTGRES_PASSWORD | Eigentümer der Datenbank: Migrationen, Richtlinien, Anlegen des Anwendungsbenutzers, Sicherung |
workoffice_app | APP_DB_PASSWORD (Name änderbar mit APP_DB_USER) | die laufende Anwendung – ohne Sonderrechte |
Die Verbindungsadressen (DATABASE_URL, DATABASE_URL_MIGRATIONS) baut docker-compose.selfhost.yml aus diesen Einträgen selbst; in die .env gehören nur die beiden Passwörter. So steht jedes Passwort an genau einer Stelle.
Warum zwei? Die Mandantentrennung steht als Row-Level-Security in der Datenbank: Jede Anfrage sieht nur die Zeilen des Betriebs, für den sie läuft. Ein Superuser oder eine Rolle mit dem Recht, Row-Level-Security zu umgehen, sähe trotzdem alles. Die Anwendung läuft deshalb als Benutzer ohne diese Rechte – dann gilt die Trennung auch dann noch, wenn in der Anwendung einmal eine Prüfung fehlen sollte. Sie ist die zweite Verteidigungslinie.
Die Passwörter sind Hex (openssl rand -hex 24), weil sie in der Verbindungsadresse stehen; ein / oder + aus Base64 würde sie zerbrechen.
Was der Start erledigt
Bei jedem Start der API, nacheinander:
- Migrationen einspielen.
- Richtlinien einspielen: die Mandantentrennung und die GoBD-Sperren. Das Skript ist wiederholbar.
- Anwendungsbenutzer anlegen bzw. sein Passwort und seine Rechte auffrischen. Hat die Rolle Superuser- oder Umgehungsrechte, bricht das ab – mit dieser Rolle wäre die Trennung wirkungslos, ohne dass irgendetwas fehlschlüge.
- Prüfen, ob die Trennung greift: Die Anwendung setzt einen Betrieb, den es nicht gibt, und zählt, wie viele Kunden sie dann sieht. Es müssen null sein.
Schlägt die Prüfung fehl, startet die API im Betrieb nicht. Im Protokoll steht dann:
Row-Level-Security greift NICHT: die Anwendung ist als Superuser oder mit BYPASSRLS verbunden. …
Meist sind dann APP_DB_USER/APP_DB_PASSWORD in der .env falsch oder leer. Ist APP_DB_USER gesetzt, das Passwort aber leer, bricht schon der Start mit einer eigenen Zeile ab – halb eingerichtet ist ein Versehen, keine Einstellung. Ebenso, wenn eine Verbindungsadresse auf localhost zeigt: Im Container ist das der Container selbst, die Datenbank heißt dort postgres.
Ob die Trennung greift, zeigt jederzeit:
curl -s http://127.0.0.1:3100/api/ready
{ "status": "ok", "mandantentrennung": "greift" }
Die GoBD-Sperren
Festgeschriebene Belege und ihre Positionen, an DATEV übergebene Eingangsbelege und das Protokoll lassen sich auch an der Anwendung vorbei nicht mehr ändern oder löschen – dafür sorgen Trigger in der Datenbank. Anders als die Row-Level-Security gelten sie für jeden Benutzer, auch für den Eigentümer. Ein Versuch endet mit einer Meldung wie:
Beleg RE-2026-0042 ist festgeschrieben und kann nicht geändert werden (GoBD)
Warum das so ist, steht unter Festschreiben und GoBD.
Von Hand in die Datenbank
Selten nötig, aber möglich:
docker compose -p workoffice -f docker-compose.selfhost.yml exec postgres \
psql -U workoffice -d workoffice
Der Eigentümer workoffice ist im Container Superuser und sieht damit alle Betriebe. Mit ihm arbeitet auch die nächtliche Sicherung – der Anwendungsbenutzer sähe wegen der Trennung nur einen Teil der Zeilen.
Eine Falle beim Passwort
PostgreSQL übernimmt POSTGRES_PASSWORD nur beim allerersten Start in ein leeres Volume. Wer es später in der .env ändert, ändert es nicht in der Datenbank – die API kommt dann nicht mehr hinein. Das Wiederherstellungsskript gleicht das Passwort deshalb nach dem Zurückspielen einer .env von selbst an.