SIMPro — Betreiber-Anleitung
Sasch Invoice Manager Pro
Server aufsetzen, betreiben, sichern und aktualisieren
Stand: 1. September 2026 · Fassung 0.3.3
Jeder Befehl in dieser Anleitung wurde am laufenden System ausgeführt, soweit das gefahrlos möglich war. Was hier steht, ist nicht abgeschrieben, sondern nachgesehen — das Protokoll liegt als docs/ANLEITUNGSABGLEICH.md daneben.
1. Überblick
Das Rechnungssystem besteht aus zwei Teilen: einem Server, der in Docker läuft und alle Daten hält, und den Apps für Windows und Android. Es gibt keine Cloud und keinen Drittanbieter dazwischen.
| Baustein | Aufgabe | Wo |
|---|---|---|
PostgreSQL (db) | Datenbank mit allen Kunden, Artikeln, Belegen, Zahlungen | Container, Daten im DATA_DIR |
API (api) | Anwendungslogik, PDF-Erzeugung, E-Mail-Versand, nächtliche Läufe | Container |
Caddy (caddy bzw. caddy-internal) | HTTPS-Zugang, Zertifikate von Let's Encrypt | Container, Port 443 |
Sicherung (backup) | Nächtliche verschlüsselte Sicherung mit restic | Container, sichert lokal und außer Haus |
| Windows-App | Bedienung am PC | Installer vom GitHub-Release |
| Android-App | Bedienung am Telefon | Play Store (interner Test) oder APK |
Der Dienst heißt
caddy-internal, wenn der Stapel hinter einem vorhandenen Webserver läuft (DEPLOY_MODE=BEHIND_PROXY), undcaddyim eigenständigen Betrieb. Werdocker compose logs -f caddyauf einer BEHIND_PROXY-Installation eingibt, bekommt eine leere Ausgabe und keinen Fehler — was leicht wie „alles ruhig" aussieht.
Der gesamte Zustand des Systems liegt in zwei Dingen: dem Ordner DATA_DIR (Datenbank, PDFs, Logos, Zertifikate) und der Datei .env (Konfiguration und Geheimnisse). Wer beides kopiert, betreibt das System auf einem anderen Rechner unverändert weiter.
2. Voraussetzungen
Hardware
Für einen einzelnen Betrieb genügt ein kleiner, sparsamer Rechner, der dauerhaft läuft:
- Empfehlung: Mini-PC mit Intel N100 oder vergleichbar, 8 GB RAM, 256 GB SSD (ab etwa 150 €). Verbrauch rund 10 Watt.
- Alternativ: ein vorhandener PC, der ohnehin läuft, oder ein NAS mit Docker-Unterstützung (Synology, QNAP).
- Kabelgebunden zum Router. WLAN ist für einen Server ungeeignet.
- Eine USV ist nicht zwingend, aber sinnvoll: ein Stromausfall mitten in einem Schreibvorgang ist die häufigste Ursache für Datenbankschäden.
Zur Belegerkennung. Seit Erweiterungsstufe B bringt das API-Abbild
tesseract-ocrsamt deutschem Sprachmodell undpoppler-utilsmit — rund 60 MB mehr im Abbild. Sie laufen auf diesem Rechner; es geht kein Beleg hinaus (siehedocs/DECISIONS.md, N19).Beim Rechenbedarf ändert das wenig: ein PDF mit Textebene wird ausgelesen und kostet Millisekunden. Nur ein fotografierter Beleg geht durch die Erkennung, und das dauert auf einem N100 wenige Sekunden je Seite. Wer viele Fotobelege erfasst, merkt es; wer PDFs bekommt, nicht.
Nachsehen lässt es sich so:
docker compose exec api tesseract --list-langsDort müssen
deuundengstehen. Fehlen sie, arbeitet alles weiter — nur ohne Vorschläge, und die Maske sagt das ausdrücklich statt eine Fehlermeldung zu zeigen.
Betriebssystem
- Ubuntu Server 24.04 LTS — empfohlen. Stabil, wartungsarm, keine Lizenzkosten.
- Windows 11 mit Docker Desktop — funktioniert, braucht aber einen angemeldeten Benutzer und ist nach Windows-Updates gelegentlich neu zu starten. Für Dauerbetrieb ist Ubuntu die bessere Wahl.
Netzwerk und Domain
- Eine eigene Domain oder Subdomain, z. B.
rechnung.firma.at. Registriert auf den Betreiber, nicht auf Sie. - DynDNS, damit die Domain immer auf die aktuelle Internet-Adresse des Routers zeigt. Die meisten Router (FRITZ!Box, Speedport) können das direkt.
- Portfreigabe: ausschließlich Port 443 (TCP) auf den Server. Kein anderer. Caddy holt die Zertifikate über TLS-ALPN-01, und das läuft über 443 — Port 80 wird nicht gebraucht.
- Eine feste lokale IP für den Server (im Router „immer dieselbe IP zuweisen"), sonst zeigt die Portfreigabe ins Leere, sobald der Router eine andere Adresse vergibt.
Konten, die vorher da sein müssen
| Konto | Wofür | Auf wen |
|---|---|---|
| Sicherungsspeicher (Hetzner Storage Box oder Backblaze B2) | Verschlüsselte Sicherung außer Haus, wenige Euro im Monat | Betreiber |
| E-Mail-Postausgang (SMTP) | Versand von Rechnungen und Mahnungen | Betreiber, meist der bestehende Mailanbieter |
| Domain mit DynDNS | Zugriff von außen | Betreiber |
| GitHub-Konto | Zugang zu Releases und Quellcode | Sie als Entwickler; Betreiber als Mitarbeiter eingeladen |
| Google Play Console | Verteilung der Android-App | Sie; Betreiber als Admin eingeladen |
3. Server vorbereiten
Ubuntu Server 24.04
Ubuntu installieren (Standardinstallation, OpenSSH-Server anhaken, kein Docker-Snap). Dann als normaler Benutzer:
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
sudo apt install -y git
# abmelden und neu anmelden, damit die Docker-Gruppe wirkt
docker --version
Automatische Sicherheitsupdates:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Windows 11 mit Docker Desktop
- PowerShell als Administrator:
wsl --install, danach Neustart. winget install Docker.DockerDesktop, Docker Desktop starten, WSL2-Backend bestätigen, kein Konto nötig.winget install Git.Git.- In Docker Desktop unter Settings → General „Start Docker Desktop when you sign in" aktivieren und in Windows die automatische Anmeldung des Benutzers einrichten, sonst läuft nach einem Neustart nichts.
- Energieoptionen: Ruhezustand und Standby auf Nie.
Docker Desktop legt seine Abbilder in einer VHDX-Datei auf C: ab, auch wenn
DATA_DIRwoanders liegt. Diese Datei wächst und schrumpft nicht von selbst. Siehe Abschnitt 8, „Aufräumen".
4. Installation
Quellcode holen
git clone https://github.com/Suschinator420/SuschiRechnungen.git rechnung
cd rechnung
Bei einem privaten Repository fragt Git nach der Anmeldung; unter Ubuntu vorher ein Personal Access Token in GitHub anlegen (Settings → Developer settings → Tokens) und als Passwort verwenden.
Einrichtungsskript ausführen
Das Skript fragt alle Werte ab, erzeugt die Geheimnisse, schreibt die .env, legt das DATA_DIR an und startet den Stapel. Es lässt sich jederzeit erneut ausführen; bestehende Werte werden als Vorgabe angeboten, Geheimnisse nie neu erzeugt.
./setup.sh # Ubuntu
.\setup.ps1 # Windows (PowerShell)
Die Fragen und die passenden Antworten für einen eigenständigen Server:
| Frage | Antwort |
|---|---|
Betriebsmodus (DEPLOY_MODE) | STANDALONE — Caddy übernimmt HTTPS selbst. BEHIND_PROXY nur, wenn bereits ein anderer Webserver auf 443 läuft |
| Domain | rechnung.firma.at (die DynDNS-Domain) |
E-Mail für Let's Encrypt (ACME_EMAIL) | Adresse des Betreibers; dorthin gehen Warnungen bei Zertifikatsproblemen |
| HTTP-/HTTPS-Port | 80 und 443 belassen |
DATA_DIR | z. B. /srv/rechnung-data (Ubuntu) oder D:\rechnung-data (Windows). Auf der SSD, nicht auf einem USB-Stick |
| SMTP-Server, Port, Benutzer, Passwort, Absender | Vom Mailanbieter des Betreibers. Port 587 mit STARTTLS ist der Normalfall |
Sicherungsziel (BACKUP_*) | Zugangsdaten des Sicherungsspeichers. Das restic-Passwort wird erzeugt und muss gesichert werden (Abschnitt 6) |
| Erster Administrator | E-Mail-Adresse des Betreibers. Das Startpasswort wird angezeigt und muss beim ersten Login geändert werden |
Nach dem Durchlauf prüfen:
docker compose ps # alle Container "healthy" bzw. "running"
curl -k https://localhost/health
Im Modus BEHIND_PROXY antwortet der Stapel nicht auf 443, sondern auf dem internen Port:
curl -s http://127.0.0.1:8080/health
Beides liefert {"status":"ok"}.
Ersteinrichtung über die App
Ein frisch aufgesetzter Server hat keinen Benutzer. Die App merkt das und zeigt statt der Anmeldung „Dieser Server ist noch nicht eingerichtet". Der Weg dahinter braucht vier Angaben — Einrichtungsschlüssel, Betriebsname, E-Mail, Passwort.
Der Schlüssel steht im Serverprotokoll und in setup-token.txt im Datenverzeichnis:
docker compose logs api | grep -i einrichtungsschluessel
cat "$DATA_DIR/setup-token.txt"
Er gilt genau einmal; danach löscht der Server ihn. Solange der Server leer ist, ist dieser Weg offen — sobald der erste Benutzer existiert, nicht mehr.
Danach steht der Betrieb, und die App zeigt die Übersicht. Was ein Betreiber dort im Blick behält, ist die unterste Kachel:

Erster Zugriff
Sobald DynDNS und Portfreigabe stehen (Abschnitt 5), holt Caddy innerhalb einer Minute das Zertifikat. Dann in der App als Serveradresse https://rechnung.firma.at eintragen und mit dem ersten Administrator anmelden. Dieselbe Adresse gilt im Firmennetz und von unterwegs.
Solange keine Domain vorhanden ist, lässt sich das System im Heimnetz testen: Caddy stellt dann ein eigenes Zertifikat aus, und die App zeigt beim ersten Verbinden dessen Fingerabdruck, den man einmalig bestätigt. Details in
docs/AUSPROBIEREN.md.
5. Zugriff von außen
- Feste IP für den Server. Im Router unter Heimnetz/Netzwerk das Gerät suchen und „immer dieselbe IPv4-Adresse zuweisen" aktivieren.
- DynDNS. Im Router unter Internet → Freigaben → DynDNS Anbieter, Domain und Zugangsdaten eintragen. Bei einer FRITZ!Box kann MyFRITZ! als DynDNS dienen; dann lautet die Domain
xyz.myfritz.netund wird als CNAME-Ziel der eigenen Domain hinterlegt. - Portfreigabe. Internet → Freigaben → Portfreigabe → Gerät wählen → neue Freigabe: Protokoll TCP, Port 443 auf 443. Sonst nichts.
- Prüfen. Vom Telefon aus im Mobilfunknetz (WLAN aus)
https://rechnung.firma.at/healthaufrufen. Es muss{"status":"ok"}erscheinen, ohne Zertifikatswarnung.
Manche Router — und Anbieter mit „DS-Lite" oder CGNAT — erlauben keine Portfreigabe über IPv4. Dann hilft ein Anruf beim Anbieter (Umstellung auf echte IPv4, oft kostenlos) oder ein VPN wie Tailscale, beschrieben in
docs/NETWORK.md.
Zweite Domain / Domainwechsel
Ein Server kann unter mehreren Namen erreichbar sein — und das ist der einzige schmerzfreie Weg, den Namen zu wechseln: erst den neuen daneben stellen, dann in Ruhe umstellen, dann den alten abschalten. Wer stattdessen umbenennt, sperrt in dem Moment jedes Gerät aus, das noch den alten Namen kennt.
So läuft es hier — die Reihenfolge ist wichtig:
- DNS. Bei Cloudflare für die neue Domain einen A-Record auf die öffentliche IP-Adresse des Anschlusses anlegen. Wichtig: DNS only, nicht „Proxied" (die orange Wolke aus). Durch den Cloudflare-Proxy käme fremdes TLS dazwischen, und die ACME-Prüfung über Port 80 fände den Server nicht.
- DDNS. Die Adresse ändert sich beim Anschluss regelmäßig. Der Router (hier ein Teltonika) hält den A-Record aktuell: unter Services → Dynamic DNS ein Profil mit Anbieter Cloudflare, der Zone, dem Hostnamen und einem API-Token, das nur
Zone.DNS: Editfür genau diese Zone darf. Ein Token mit mehr Rechten hat auf einem Router nichts verloren. - Warten und nachsehen.
nslookup rechnung.neue-domain.atmuss die richtige Adresse liefern, bevor es weitergeht. Ein Zertifikat für einen Namen, der noch woandershin zeigt, gibt es nicht. - Server-Block. Vor dem Rechnung-Stack steht auf diesem Rechner ein nginx. Der neue Name kommt in
B:\nginx\conf\sites\rechnung.conf: ein Port-80-Block (ACME-Ablage plus Umleitung auf HTTPS, für beide Namen derselbe) und ein eigener Port-443-Block je Name. Was beide gemeinsam haben — Kopfzeilen, Zeitlimits, die Weiterleitung an127.0.0.1:8080— steht einmal inrechnung-weiterleitung.incund wird zweimal eingebunden. Zwei Kopien desselben Blocks wären der sichere Weg, dass eines Tages nur eine davon gepflegt wird. - Zertifikat. Erst den Port-443-Block weglassen, sonst startet nginx nicht: er verlangt eine Zertifikatsdatei, die es noch nicht gibt. Also: Port-80-Block einspielen,
nginx -t,nginx -s reload, dann
`` B:\win-acme\wacs.exe --target manual --host rechnung.neue-domain.at ^ --friendlyname "rechnung.neue-domain.at" ^ --validation filesystem --webroot B:\nginx\acme-webroot ^ --store pemfiles --pemfilespath B:\nginx\certs\rechnung-neu ^ --installation script --script B:\nginx\acme-reload.cmd --accepttos ``
Den Zielordner vorher anlegen — win-acme legt ihn nicht selbst an und bricht sonst mit invalid --pemfilespath ab. Danach den Port-443-Block ergänzen, wieder nginx -t und nginx -s reload.
- Prüfen. Beide Namen müssen
200liefern und ein gültiges, auf den jeweiligen Namen ausgestelltes Zertifikat zeigen:
`` curl -o NUL -w "%{http_code}\n" https://rechnung.alte-domain.at/health curl -o NUL -w "%{http_code}\n" https://rechnung.neue-domain.at/health ``
- Verlängerung. win-acme merkt sich jede Bestellung und verlängert sie, wenn es regelmäßig aufgerufen wird. Eine tägliche geplante Aufgabe
B:\win-acme\wacs.exe --renewerledigt das für alle Zertifikate auf einmal; nach jeder Verlängerung ruftacme-reload.cmdeinnginx -s reloadauf, damit das neue Zertifikat auch ausgeliefert wird. Der Aufruf läuft ohne Nachfrage durch und ist ohne fällige Bestellung in Sekunden fertig.
win-acme bringt seine Aufgabe selbst mit. Sie heißt win-acme renew (acme-v02.api.letsencrypt.org) und trägt die Einstellungen aus settings.json: Start 09:00 plus eine Zufallsverzögerung von bis zu vier Stunden — damit nicht alle Welt zur selben Minute bei Let's Encrypt anklopft —, Zeitlimit zwei Stunden, Erneuerung ab 55 Resttagen. wacs.exe pflegt sie bei jeder Aktualisierung mit. Das ist die Aufgabe, die bleiben soll.
Sie muss unter dem Systemkonto laufen. Eine Aufgabe im Modus nur interaktiv läuft nur, solange jemand angemeldet ist — nach einem Neustart, bei dem sich niemand anmeldet, läuft sie nie. Zertifikate laufen nach 90 Tagen ab, und ein Server, der durchläuft, aber niemanden angemeldet hat, fällt dann still aus.
Nachsehen lässt sich das nur erhöht: eine Aufgabe des Systemkontos ist für einen normalen Benutzer nicht einmal lesbar.
``powershell # PowerShell als Administrator Get-ScheduledTask | Where-Object { $_.TaskName -match 'acme|wacs' } | Select-Object TaskName, State, @{n='Konto';e={$_.Principal.UserId}} ``
Fehlt sie oder läuft sie nicht unbeaufsichtigt, legt B:\Rechnung-privat\zertifikatsaufgabe-einrichten.ps1 eine unter SYSTEM an (ebenfalls erhöht). Stehen am Ende zwei Aufgaben da — die eigene und eine von Hand angelegte —, räumt B:\Rechnung-privat\zertifikatsaufgabe-aufraeumen.ps1 auf: es vergleicht beide, prüft, ob die bleibende wirklich unbeaufsichtigt läuft, und löscht erst dann die andere. Eine Aufräumaktion, die am Ende gar keine Verlängerung hinterlässt, wäre schlimmer als zwei.
Wann die Apps umgestellt werden — zuletzt. Solange beide Namen bedient werden, ist nichts eilig. Der Server nimmt jeden der beiden an; welchen ein Gerät verwendet, steht in dessen Einstellungen unter Serveradresse. Sinnvolle Reihenfolge: erst den neuen Namen einige Tage mitlaufen lassen, dann Gerät für Gerät umstellen (und dabei prüfen, dass die Anmeldung geht), dann eine Weile warten, und erst danach den alten Namen aus nginx nehmen. Das Zertifikat des alten Namens läuft von selbst aus; die Bestellung dazu gehört dann mit wacs.exe --cancel entfernt, sonst versucht die Verlängerung monatelang etwas, das niemand mehr braucht.
Überwachung von außen
/health antwortet ohne Anmeldung und verrät nichts über den Betrieb — genau so viel, wie eine Überwachung braucht:
{"status":"ok"}
Ein Dienst wie UptimeRobot prüft alle fünf Minuten auf diese Adresse und meldet sich, wenn der Server nicht antwortet.
6. Sicherung
Was gesichert wird und wie
Der Sicherungs-Container erstellt jede Nacht einen Datenbank-Auszug und sichert ihn zusammen mit dem Dateispeicher (PDFs, Logos, Belegscans) verschlüsselt per restic — in drei Ziele: lokal im DATA_DIR, auf die externe Platte, und auf den Speicher außer Haus. Alle drei mit demselben Passwort, jedes als eigenes Repository. Aufbewahrt werden 7 tägliche, 4 wöchentliche, 12 monatliche und 7 jährliche Stände.
Bleibt ein Lauf aus oder schlägt fehl, geht eine E-Mail an den Administrator, und die Übersicht in der App zeigt es. Ausführlicher steht es unter Verwaltung → Server, Updates und Sicherung:

Die 3-2-1-Regel
Die Regel, an der sich jede Sicherung messen lässt:
Drei Kopien der Daten, auf zwei verschiedenen Medien, eine davon außer Haus.
So sieht das hier aus:
| Kopie | Wo | Wogegen sie hilft |
|---|---|---|
| Die Daten selbst | im DATA_DIR auf der Serverplatte | — |
| Sicherung 1 | daneben, im DATA_DIR | Ein Fehlgriff: gelöschter Kunde, verkorkster Import |
| Sicherung 2 | externe Platte unter R:\rechnung-backup | Der Ausfall der Serverplatte |
| Sicherung 3 | Speicher außer Haus (restic-Ziel) | Feuer, Diebstahl, Wasser |
Die erste Sicherung liegt auf derselben Platte wie die Daten. Fällt die aus, sind beide weg — und man hängt an einer Leitung, die vielleicht gerade nicht da ist. Deshalb die zweite auf einem eigenen Medium.
Die externe Platte ersetzt den Speicher außer Haus nicht. Sie steht neben dem Server, im selben Raum. Was den Server trifft — Feuer, Wasser, ein Einbrecher, der den Rechner mitnimmt —, trifft sie mit. Sie ist die zweite Kopie auf einem zweiten Medium, nicht die dritte an einem anderen Ort.
Die Platte dient nur der Sicherung. Keine Urlaubsbilder, keine Installationsdateien, kein „da ist ja noch Platz". Eine Platte, auf der auch anderes liegt, wird irgendwann für anderes gebraucht — und ist dann in dem Moment nicht da, in dem es darauf ankommt. Sie bleibt außerdem angesteckt; eine Sicherung, an die man denken muss, findet nicht statt.
Die externe Platte einrichten
Einmalig, in PowerShell im Projektverzeichnis:
.\scripts\backup-extern-einrichten.ps1
Das Skript prüft Laufwerk, Platz und Dateisystem, legt R:\rechnung-backup an, setzt die Marke (siehe unten) und trägt die beiden Werte in die .env ein. Danach:
docker compose up -d backup
docker compose exec backup /usr/local/bin/backup.sh # den ersten Lauf gleich
Der Laufwerksbuchstabe muss fest sein. Wandert er beim nächsten Anstecken, findet die Sicherung ihr Ziel nicht mehr. Fest zuweisen in der Datenträgerverwaltung → Rechtsklick auf die Partition → Laufwerkbuchstaben und -pfade ändern.
Die Marke. Im Zielverzeichnis liegt eine Datei .rechnung-sicherungsziel. An ihr erkennt die Sicherung, dass die Platte wirklich da ist. Das ist kein Umstand, sondern notwendig: Docker legt einen fehlenden Pfad als leeres Verzeichnis an, und ohne Marke sähe eine abgesteckte Platte genau aus wie eine leere. Die Sicherung liefe dann klaglos auf die interne Platte, in ein Verzeichnis, das niemand je ansieht — es sähe aus, als funktionierte alles. Löschen Sie die Datei nicht.
Die Falle: Docker sieht die Platte nicht
Diese kostet einen Nachmittag, wenn man sie nicht kennt.
Docker Desktop läuft auf Windows in einer eigenen kleinen Linux-Maschine (WSL). Die hängt die Windows-Laufwerke beim Hochfahren ein. Eine Platte, die danach angesteckt wird, gibt es dort nicht — auch wenn Windows sie längst anzeigt.
Und dann passiert das Unangenehme: Docker legt den fehlenden Pfad als leeres Verzeichnis innerhalb der Linux-Maschine an. Auf der Windows-Seite stimmt alles — Ordner da, Marke da, .env eingetragen. Im Container ist das Verzeichnis leer. Die Sicherung findet die Marke nicht, hält die Platte für abgesteckt und überspringt sie. Jede Nacht, ohne dass etwas kaputtgeht.
Deshalb prüft das Einrichtungsskript von der anderen Seite. Es startet einen Wegwerf-Container mit genau demselben Pfad und sieht nach, ob die Marke dort ankommt. Sagt es Ja, ist die Sache erledigt. Sagt es Nein, nennt es die Abhilfe und beendet sich mit Rückgabewert 2.
Die Abhilfe ist eine von zweien:
- Docker Desktop neu starten (Rechtsklick im Infobereich → Restart). Beim Hochfahren findet die Maschine die Platte und hängt sie ein. Das ist der saubere Weg.
- Nachreichen ohne Neustart, wenn gerade etwas läuft, das nicht unterbrochen werden soll:
``powershell wsl -d docker-desktop -e mount -t drvfs R: /mnt/host/r docker compose up -d --force-recreate backup ``
Das --force-recreate ist nötig: der Container erbt seine Einhängepunkte beim Anlegen. Ein laufender Container sieht die nachgereichte Platte nicht. Diese Lösung hält bis zum nächsten Start der Linux-Maschine.
Damit die Frage gar nicht aufkommt: die Platte angesteckt lassen. Dann ist sie beim nächsten Hochfahren da und wird von selbst eingehängt. Das ist ohnehin gemeint — eine Sicherung, an die man denken muss, findet nicht statt.
Nur ein Lauf auf einmal
Der Container stößt beim Start eine Sicherung an, daneben steht der Zeitplan, und dazwischen tippt jemand den Lauf von Hand an. Treffen sich zwei, räumt der eine dem anderen das Arbeitsverzeichnis weg und hält das Repository gesperrt — das Ergebnis sieht aus wie ein kaputtes Repository, ist aber nur ein Zusammenstoß.
Deshalb hält sich jeder Lauf eine Sperre. Wer als Zweiter kommt, schreibt eine Zeile ins Protokoll und geht:
[…] ein anderer Lauf ist noch unterwegs - dieser hier entfaellt
Das ist kein Fehler und wird auch nicht als solcher vermerkt. Es lief schon.
Wenn die Platte nicht angesteckt ist
Der Lauf geht trotzdem durch: die interne Sicherung und der Speicher außer Haus werden geschrieben. Für die Platte kommt ein eigener Eintrag mit dem Zustand Übersprungen — nicht Fehlgeschlagen, denn fehlgeschlagen ist nichts.
- Bis drei Tage: eine Notiz im Protokoll. Wer die Platte übers Wochenende mitnimmt, soll nicht jeden Morgen eine Warnung bekommen — sonst sieht er nach einer Woche keine mehr an, auch nicht die, auf die es ankommt.
- Ab dem vierten Tag: eine Warnung.
Was regelmäßig geprüft wird
| Wann | Was | Wo |
|---|---|---|
| Jeden Lauf | Die Sicherung selbst, danach das Ausdünnen alter Stände | beide lokalen Ziele und das Ziel außer Haus |
| Sonntags | restic check --read-data-subset=5% — eine rollende Leseprobe gegen Bitfäule | alle Ziele |
| Am Monatsersten | restic check vollständig — ob der Baum überhaupt zusammenhängt | alle Ziele |
Die Leseprobe sieht 5 Prozent der Daten an, die monatliche Prüfung die ganze Struktur. Ein Repository, dem der Index fehlt, fällt sonst erst bei der Wiederherstellung auf — also an dem Tag, an dem man es braucht.
Aus der externen Platte wiederherstellen
Wie sonst auch, nur mit dem anderen Repository:
# Was auf der Platte liegt
docker compose run --rm -e RESTIC_REPOSITORY=/backup-extern/restic \
restore restic snapshots
# Und zurückspielen
docker compose run --rm -e RESTIC_REPOSITORY=/backup-extern/restic \
restore restore.sh --snapshot <id>
Üben Sie das einmal, bevor Sie es brauchen. Eine Sicherung, aus der noch nie jemand etwas zurückgeholt hat, ist eine Vermutung. Am besten in eine Wegwerf-Datenbank, damit der laufende Betrieb unberührt bleibt — wie es in
docs/BACKUP.mdsteht.
Das restic-Passwort
Das restic-Passwort aus der
.envist der einzige Schlüssel zu allen Sicherungen. Ohne dieses Passwort ist jedes Backup wertlos. Es gehört in den Passwortmanager des Betreibers und zusätzlich auf Papier an einen sicheren Ort. Dasselbe gilt für die Zugangsdaten des Sicherungsspeichers.
Die Befehle
# Eine Sicherung von Hand anstoßen
docker compose run --rm backup backup.sh
# Was gesichert ist, auflisten
docker compose run --rm backup restore.sh --list
# Einen Stand zurückspielen
docker compose run --rm restore restore.sh --snapshot <snapshot-id>
Die Ausgabe eines Laufs sieht so aus:
[2026-08-30T12:26:37+02:00] backup started (target LOCAL)
[2026-08-30T12:26:37+02:00] dumping database
[2026-08-30T12:26:38+02:00] backing up to /backup/local
[2026-08-30T12:26:39+02:00] pruning /backup/local
[2026-08-30T12:26:40+02:00] weekly integrity check on /backup/local
[2026-08-30T12:26:41+02:00] backup finished (snapshot b95ac899)
Nicht
restic snapshotsund nichtbackup-now. Beides steht in älteren Notizen und beides scheitert:resticallein kennt das Repository nicht („Please specify repository location"), undbackup-nowgibt es nicht („exec: backup-now: not found"). Die drei Befehle oben sind die richtigen.
Wiederherstellung üben
Eine Sicherung, die nie zurückgespielt wurde, ist keine Sicherung. Der Ablauf ist in docs/BACKUP.md mit Übungsprotokoll beschrieben. Beim Test auf dem Entwicklungssystem dauerte ein vollständiges Zurückspielen unter 20 Sekunden.
Empfehlung: einmal nach der Installation und danach jährlich — am besten auf einem zweiten Rechner, damit der Ernstfall „Server ist tot, neuer Rechner steht da" wirklich durchgespielt ist.
7. Updates einspielen
Seit Fassung 0.1.16 hält sich der Server selbst auf dem Laufenden — nicht heimlich, sondern nachvollziehbar: er sieht nach, er sagt Bescheid, und einspielen tut es ein Skript auf dem Rechner. Was von Hand ging, geht weiter von Hand; es muss nur nicht mehr.
Was von selbst passiert
Jede Nacht um 02:00 fragt der Server bei GitHub nach, ob es eine neuere Fassung gibt. Findet er eine, steht sie in der App unter Einstellungen → Programm → Server: die Nummer, die Release-Notizen und, was als Nächstes geschieht. Zugleich geht eine Benachrichtigung an alle Administratoren — Glocke und E-Mail.
Ob eine Fassung von selbst eingespielt wird, entscheidet nicht der Server, sondern die Release-Notiz. Steht dort eine Zeile
update: auto
läuft sie im nächsten Wartungsfenster (Vorgabe 03:00, einstellbar über UPDATE_WINDOW in der .env). Steht dort update: manual — oder steht dort gar nichts —, wartet sie auf den Knopf Jetzt einspielen. Das Fehlen der Markierung gilt als manual, und das ist Absicht: eine Fassung, bei der niemand darüber nachgedacht hat, ob sie unbeaufsichtigt laufen darf, läuft nicht unbeaufsichtigt. Vorabfassungen (pre-release) laufen nie von selbst.
Damit überhaupt etwas laufen kann, muss das Updateskript regelmäßig aufgerufen werden — es fragt den Server, ob etwas ansteht, und tut sonst nichts:
# Linux: stündlich in der Crontab des Benutzers, dem der Stack gehört
0 * * * * cd /opt/rechnung && ./scripts/update.sh >> data/logs/update.log 2>&1
# Windows: eine geplante Aufgabe, stündlich
schtasks /create /tn "Rechnung Update" /sc hourly `
/tr "powershell -File B:\Rechnung\scripts\update.ps1"
Damit das Skript den Server fragen und ihm melden kann, braucht es einen Zugang. In der .env genügt eines von beidem:
UPDATE_TOKEN=… ein fertiges Zugriffstoken (bei Zwei-Faktor der
bequemere Weg — erneuern, wenn es abläuft)
UPDATE_USER=… oder Benutzer und Passwort eines Administrators
UPDATE_PASSWORD=…
Ohne Zugang läuft ein Update trotzdem durch, wenn es von Hand angestoßen wird — es steht dann nur nichts davon in der Oberfläche.
Was der Knopf tut
Jetzt einspielen setzt eine Markierung. Mehr nicht — und das hat einen Grund: der Server steckt in einem Container. Er kann sich nicht selbst anhalten, kein Abbild ziehen und sich nicht wieder starten, und er soll es auch nicht können: kein Container dieses Stapels bekommt den Docker-Socket, denn wer den hat, ist auf dem Rechner root. Der Server entscheidet also, das Skript auf dem Rechner tut. Beim nächsten stündlichen Lauf holt es sich den Auftrag ab.
Zwischen Knopf und Ausführung liegt deshalb bis zu eine Stunde. In der App steht in dieser Zeit, dass es angefordert ist.
Der Ablauf, und warum er so ist
scripts/update.sh (bzw. update.ps1) macht immer dasselbe, in dieser Reihenfolge:
- Sicherung — vor allem anderen. Eine Migration lässt sich nicht zurückdrehen; der restic-Schnappschuss ist der einzige Weg zurück. Scheitert die Sicherung, wird nicht aktualisiert.
- Abbild ziehen — vor dem Anhalten. Wer erst anhält und dann zieht, steht bei einem Netzfehler mit einem toten Server da.
- Anhalten, starten. Die Migrationen laufen beim Start des Containers.
/health— mit Zeitlimit (UPDATE_HEALTH_TIMEOUT, Vorgabe 180 s).- Melden — Erfolg wie Fehlschlag, mit dem vollständigen Protokoll.
Wenn es schiefgeht
Wird der Server nicht gesund, rollt das Skript von selbst zurück: API anhalten, Datenbank aus dem Schnappschuss von Schritt 1 zurückspielen, altes Abbild starten, warten, bis er wieder antwortet. Erst dann meldet es.
Das Zurückspielen der Datenbank ist der wichtige Teil. Eine Migration, die zur Hälfte lief, hinterlässt einen Eintrag „fehlgeschlagen" — und danach startet auch die alte Fassung nicht mehr, denn Prisma verweigert den Start, solange eine gescheiterte Migration in der Datenbank steht. Ohne den Schnappschuss stünde man dann vor einem Server, der weder vorwärts noch rückwärts geht.
Die Belegdateien werden dabei bewusst nicht angefasst: ein Update ändert die Datenbank, nicht den Dateispeicher.
Erfolg und Fehlschlag gehen als Benachrichtigung an alle Administratoren — Glocke und E-Mail. Das Protokoll steht in der App unter Einstellungen → Programm → Server, aufklappbar, Schritt für Schritt mit Uhrzeit.
Eine Einschränkung, die man kennen sollte. Rollt ein Update auf eine Fassung zurück, die älter als 0.1.16 ist, kann das Skript den Fehlschlag nirgends melden — die alte Fassung kennt den Endpunkt dafür noch nicht. Das Protokoll steht dann nur auf dem Rechner. Ab 0.1.16 kommt jede Meldung an.
Von Hand
Geht weiterhin, und für einen großen Sprung ist es der ruhigere Weg:
cd rechnung
./scripts/update.sh --pruefen # was anstünde
./scripts/update.sh --fassung 0.1.17 # genau diese einspielen
Ohne --fassung fragt es den Server und tut nichts, solange nichts angefordert ist.
Der Weg der Apps: über den Server, nicht über GitHub
Seit 0.2.2 fragt keine App mehr selbst bei GitHub nach. Sie fragt ihren eigenen Server (/api/v1/updates/latest), und der fragt GitHub. Was zurückkommt: die Fassung, die Release-Notizen, je Datei ihre Prüfsumme aus SHA256SUMS.txt — und eine Adresse, die wieder auf den eigenen Server zeigt (/api/v1/updates/datei/…). Von dort holt die App den Installer.
Drei Gründe, und der dritte hat es ans Licht gebracht:
- Eine App in einem Netz, das nur den eigenen Server erreichen darf, kommt trotzdem an ihre neue Fassung.
- Ein privates Repository braucht ein Token auf dem Server statt eines auf jedem Arbeitsplatz.
- Ohne Token antwortet GitHub mit
404, nicht mit403. Die Prüfung fällt also nicht auf — sie findet nur nie etwas. Genau das war monatelang der Fall, ohne dass es jemandem auffiel.
Der Weiterleitungsdienst ist keiner: der Server gibt nur Dateien heraus, die als Anhang genau dieses Releases dastehen, und nur an angemeldete Benutzer.
Das Token für ein privates Repository
Ist das Repository nicht öffentlich, braucht der Server ein Zugriffstoken:
# .env auf dem Server
UPDATE_GITHUB_TOKEN=github_pat_…
Ein fein abgestuftes Token mit genau einer Berechtigung: Lesezugriff auf die Contents (dazu gehören die Releases) des einen Repositories. Kein repo-Vollzugriff, kein Token eines Menschen, kein Token der GitHub-CLI — ein Server, der nachts nach Aktualisierungen sieht, braucht nichts, womit sich Code ändern lässt.
Fehlt es, sagt die Prüfung es im Klartext; die Meldung steht in der App unter Einstellungen → Programm → Server.
Wenn das Token abläuft
Ein fein abgestuftes Token hat ein Ablaufdatum. Und ein abgelaufenes Token bringt bei GitHub dieselbe Antwort wie gar keines: 404. Die Prüfung fällt also nicht aus — sie hört auf, etwas zu finden, und das sieht aus wie „alles aktuell".
Deshalb überwacht der Server das Datum:
- Es kommt aus der Antwort von GitHub selbst (Kopfzeile
github-authentication-token-expiration) — das ist die verlässliche Quelle, denn sie stammt vom Aussteller. - Fehlt sie, gilt
UPDATES_TOKEN_EXPIRES=2027-09-01aus der.env. - Steht beides nicht da, wird nicht gewarnt. Eine Warnung über ein Datum, das niemand kennt, wäre geraten.
Ab 30 Tagen vorher steht die Warnung unter Einstellungen → Programm → Server, und alle Administratoren bekommen eine Benachrichtigung — Glocke und E-Mail, höchstens einmal am Tag. Eine Warnung, die jede Nacht neu kommt, wird nach der dritten nicht mehr gelesen und fehlt dann auch am letzten Tag.
Erneuern heißt: neues Token auf GitHub erzeugen, in die .env eintragen, docker compose up -d api. Das Datum daneben nicht vergessen, falls GitHub es nicht selbst mitschickt.
Für Shopify gibt es das nicht. Der Zugang eines eigenen Apps („custom app") läuft nicht ab, und Shopify nennt in seinen Antworten kein Datum. Was dort passieren kann, ist etwas anderes: der Zugang wird entfernt. Das merkt der Server sofort — Shopify schickt app/uninstalled, der Laden wird abgeschaltet und trägt den Grund.
Wo die Abbilder herkommen### Wo die Abbilder herkommen
Jedes Release veröffentlicht das Server-Abbild nach ghcr.io/suschinator420/suschirechnungen, mit der Fassungsnummer und mit latest. Das Skript zieht von dort; eine andere Quelle lässt sich über UPDATE_IMAGE in der .env eintragen.
Apps
- Windows: Die App sieht beim Start bei GitHub nach. Gibt es eine neuere Fassung, fragt sie nach: „Neue Fassung x.y.z verfügbar — jetzt installieren?" Wer zustimmt, sieht den Download, danach prüft die App die Datei gegen
SHA256SUMS.txtaus demselben Release, startet den Installer und beendet sich. Wer ablehnt, wird beim nächsten Start wieder gefragt. - Android über Play Store: Updates kommen von selbst, sobald ein Release im internen Testkanal liegt.
- Android über APK: Neue APK vom Release herunterladen und installieren. Die Anmeldung bleibt erhalten, solange dieselbe Signatur verwendet wird — das stellt der Workflow sicher.
Reihenfolge: erst Server, dann Apps. Der Server bedient Apps der vorherigen Minor-Fassung weiter — welche das ist, sagt er selbst unter /api/v1/version. Eine App, die zu alt ist, sagt es beim Anmelden und lässt sich nicht wegklicken; eine ältere, aber noch unterstützte weist einmal freundlich darauf hin und arbeitet normal weiter.
Der Windows-Installer von Hand
Der übliche Weg ist die App selbst: Einstellungen → Programm, dort steht die neue Fassung, und ein Druck darauf lädt sie, prüft die Prüfsumme, startet den Installer und beendet die App — genau in dieser Reihenfolge, denn ein Installer kommt an eine offene Datei nicht heran.
Wer ihn von Hand startet, sollte zwei Dinge wissen.
Er verlangt keine Administratorrechte. Die Installation liegt unter %LOCALAPPDATA%\Programs\Rechnung und gehört dem angemeldeten Benutzer (PrivilegesRequired=lowest). Es gibt deshalb auch keine Rückfrage der Benutzerkontensteuerung — wer auf eine wartet, wartet vergebens.
Er scheitert, solange die App läuft. Der Installer versucht sie über den Neustart-Manager von Windows zu schließen; gelingt das nicht, kann er rechnung.exe nicht ersetzen:
DeleteFile: The existing file appears to be in use (5). Retrying.
Nach fünf Versuchen kommt eine Rückfrage Abbrechen / Wiederholen / Ignorieren. Mit /SUPPRESSMSGBOXES wird sie automatisch mit Abbrechen beantwortet — der Lauf rollt zurück und endet mit Rückgabewert 5. Von außen sieht das aus, als sei nichts geschehen.
Also: die App vorher schließen. Dann läuft auch
rechnung-setup-x.y.z.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTARTdurch.
Das Protokoll steht unter %TEMP%. Seit 0.4.1 schreibt der Installer bei jedem Lauf eines — auch beim stillen —, als Setup Log <Datum> #<Nummer>.txt. Davor gab es nur mit /LOG=<Datei> eines, und ein gescheiterter stiller Lauf hinterließ nichts: das ist der Grund, warum ein hängengebliebenes Update einmal aussah, als schriebe der Installer „nicht einmal sein Protokoll".
| Rückgabewert | Was er heißt |
|---|---|
| 0 | durchgelaufen |
| 1 | Setup konnte nicht starten |
| 2 | im Assistenten abgebrochen, bevor installiert wurde |
| 5 | während der Installation abgebrochen — fast immer die laufende App |
| 8 | ein Neustart ist nötig |
8. Laufender Betrieb
Was regelmäßig zu prüfen ist
| Wie oft | Was | Wo |
|---|---|---|
| Wöchentlich | Letzte Sicherung erfolgreich, Datenbankgröße, Protokollkette intakt | App → Übersicht, oder Verwaltung → Server, Updates und Sicherung |
| Monatlich | Betriebssystem-Updates eingespielt, Container laufen, Plattenplatz | SSH bzw. Docker Desktop |
| Bei neuem Release | Server aktualisieren, dann Apps | Abschnitt 7 |
| Jährlich | Wiederherstellung üben, Konto beim Sicherungsspeicher und Domain verlängert | Abschnitt 6 |
Protokoll-Kette prüfen rechnet die Prüfsummen des Änderungsprotokolls nach. Sie hängen aneinander; ein nachträglich geänderter Eintrag bricht die Kette. Der Knopf sitzt unten im Serverstatus.
Die App warnt von selbst, wenn unter 10 Prozent Plattenplatz frei sind. Auf derselben Platte liegen Datenbank, Dateien und Sicherung; eine Sicherung, die mitten im Schreiben auf eine volle Platte läuft, hinterlässt kein Backup, sondern ein halbes.
Was der Server nachts von selbst tut
| Zeit | Was |
|---|---|
| 3:30 | Aufräumen: abgelaufene Token, Anmeldeversuche älter als 90 Tage, Abgleichvorgänge älter als 30 Tage, Meldungen älter als 90 Tage |
| 3:30 | Aufbewahrungsbericht: was nach sieben Jahren löschbar wäre. Gelöscht wird nichts — das ist eine Entscheidung des Betriebs, nicht eines Cron-Auftrags |
| 3:30 | Meldungen erzeugen: überfällige Rechnungen, fällige Abos, Artikel unter Mindestbestand, ausgebliebene Sicherung, Mahnvorschläge |
| 3:30 | Bankumsätze holen, falls ein Konto verbunden ist — und an Zustimmungen erinnern, die in weniger als 14 Tagen ablaufen |
| 3:30 | Shopify-Bestellungen zu Aufträgen machen, falls ein Laden angebunden ist |
| stündlich | Die täglichen Zusammenfassungen verschicken, für alle, deren eingestellte Stunde gerade geschlagen hat |
| 3:15 (Vorgabe) | Die Sicherung, siehe Abschnitt 6 |
Alles davon steht im Anwendungsprotokoll — auch wenn nichts anliegt. Eine Prüfung, die im stillen Fall gar nichts schreibt, lässt sich von einer Prüfung, die nicht läuft, nicht unterscheiden.
Bankumsätze selbst abholen (freiwillig)
Ohne diesen Abschnitt bleibt es beim Einlesen von Kontoauszugsdateien. Das funktioniert unverändert und wird durch nichts hiervon angetastet.
Der Unterschied ist nicht die Genauigkeit, sondern wer daran denken muss. Eine Datei liest jemand ein, wenn er daran denkt; wer zweimal im Monat daran denkt, mahnt Kunden, die vor drei Wochen bezahlt haben.
Was Sie brauchen
Ein Konto bei einem PSD2-Aggregator. Enable Banking ist der Weg, den man heute gehen kann; GoCardless Bank Account Data nimmt seit Juli 2025 keine neuen Kunden mehr an (der Adapter dafür ist da und nützt nur, wer schon einen Zugang hat).
Dort eine Anwendung anlegen. Heraus kommen eine Anwendungskennung und eine private Schlüsseldatei (.pem).
**Was Sie nicht brauchen: ein eigenes eIDAS-Zertifikat (QWAC).** Das kostet 3.000 bis 8.000 Euro im Jahr und ist der Grund, warum es in Europa kaum kostenlose Bankschnittstellen gibt. Nötig ist es nur für den, der selbst Zahlungsdienstleister wird. Über einen Aggregator ist der Aggregator der lizenzierte Teilnehmer, nicht Sie.
Was in die .env kommt
ENABLEBANKING_APPLICATION_ID=<aus dem Kundenbereich>
ENABLEBANKING_PRIVATE_KEY=/etc/rechnung/enablebanking.pem
BANK_REDIRECT_URL=https://<Ihre Domain>/einstellungen/bankkonten/zurueck
Die Schlüsseldatei muss für den Container lesbar sein — legen Sie sie neben die .env und binden Sie sie ein, oder tragen Sie einen Pfad ein, der im Container existiert. Sie gehört nicht ins Repository und nicht in eine Sicherung, die jemand anders lesen kann: mit ihr wird jede Anfrage unterschrieben.
Die Rückkehradresse muss beim Anbieter als erlaubt hinterlegt sein. Fehlt sie dort, bricht die Anmeldung ganz am Ende ab — mit einer Meldung der Bank, die niemand versteht.
Danach docker compose up -d --force-recreate api.
Was der Betrieb dann tut
Einstellungen → Bankkonten → Konto verbinden. Die Anmeldung findet im Browser bei der Bank statt; dieses Programm sieht die Bankzugangsdaten nie und kann nur lesen, nicht überweisen.
Die erste Probe ist die Bankliste. Steht die eigene Bank nicht drin, nützt der Abgleich für dieses Konto nichts — dann bleibt der Weg über die Datei.
Was Sie im Auge behalten müssen
Die Zustimmung läuft nach höchstens 90 Tagen ab. Das ist PSD2 und keine Einschränkung dieses Programms. Danach kommen keine Umsätze mehr — und zwar still: es gibt keine Fehlermeldung, es sieht aus wie eine ruhige Woche.
Dagegen zwei Dinge: der nächtliche Lauf meldet ab 14 Tagen vorher, und der Bildschirm Bankkonten zeigt die Restlaufzeit auch dann, wenn noch zwei Monate Zeit sind.
Die Automatik
Je Konto einstellbar und voreingestellt aus. Sie bucht eine Zahlung ohne Rückfrage nur, wenn alle drei zutreffen:
- Die Belegnummer steht im Verwendungszweck, als eigenes Wort
- Der Betrag stimmt auf den Cent mit dem offenen Posten
- Es passt genau ein Beleg
Alles andere landet wie bisher in der Vorschlagsliste. Warum so eng: wer täglich vierzig Überweisungen bestätigt, von denen achtunddreißig eindeutig sind, klickt nach einer Woche ohne zu lesen — und übersieht die zwei, bei denen es darauf angekommen wäre.
Jede automatische Buchung wird gemeldet (Zahlung automatisch zugeordnet, voreingestellt an) und lässt sich zurücknehmen. An jeder Zeile steht, warum sie gebucht wurde — oder warum nicht.
Einen Shopify-Laden anbinden (freiwillig)
Wer online verkauft, tippt die Bestellungen sonst ab. Bei zehn am Tag sind das zehn Gelegenheiten, sich zu vertippen — bei Beträgen, die später auf einer Rechnung stehen.
Was in Shopify anzulegen ist
Eine eigene App (custom app), keine aus dem App Store:
- Im Shop-Admin: Einstellungen → Apps und Vertriebskanäle → App entwickeln
- App anlegen, Admin-API-Berechtigungen wählen:
| Berechtigung | Wofür | Nötig? | |---|---|---| | read_orders | Bestellungen nachholen, wenn ein Webhook verloren geht | ja | | read_products | Die SKU einer Bestellung dem Artikel zuordnen | ja | | read_customers | Kundendaten der Bestellung | ja | | read_locations | Die Lagerorte des Shops anzeigen | nur mit Rückschreiben | | read_inventory | Den Bestand im Shop lesen | nur mit Rückschreiben | | write_inventory | Den Bestand im Shop setzen | nur mit Rückschreiben |
Wer älter als 60 Tage zurückschauen will, braucht zusätzlich read_all_orders — das vergibt Shopify nur auf Antrag.
Pflicht dazu: write_webhooks (und read_webhooks). Ohne sie kann der Server die Webhooks nicht selbst anlegen; es käme dann keine Bestellung von allein an, nur der nächtliche Abgleich holte sie nach. Der Verbindungstest benennt den fehlenden Scope.
Empfohlen dazu: read_draft_orders. Nicht für den Betrieb — es wird nie ein Entwurf übernommen —, sondern für die Diagnose. Ein Entwurf sieht im Shopify-Admin wie eine Bestellung aus, ist aber keine und löst keinen Webhook aus. Mit dieser Berechtigung sagt der Verbindungstest, dass welche liegen; ohne sie steht dort nur, dass er es nicht sehen kann.
- Installieren. Heraus kommt ein Admin-API-Zugriffstoken (
shpat_…)
Dann in diesem Programm: Einstellungen → Shopify → Laden eintragen, mit der Ladenadresse (mein-laden.myshopify.com), dem Token und dem API-Geheimschlüssel der App (shpss_…, in Shopify bei der App unter den API-Zugangsdaten).
Die Webhooks legt der Server selbst an — beim Eintragen, beim Einschalten und bei jedem Verbindungstest. Von Hand ist dort nichts mehr einzutragen. Er legt an, was fehlt, richtet eine falsche Adresse, ersetzt einen, der in einer alten Fassung zugestellt wird, und entfernt eigene Einträge zu Themen, die dieses Programm nicht mehr verarbeitet:
| Ereignis | Wofür |
|---|---|
orders/create | Die neue Bestellung |
orders/updated | Die nachträglich bezahlte |
orders/cancelled | Damit aus einer stornierten Bestellung kein Auftrag mehr wird |
app/uninstalled | Wird der Zugang entfernt, schaltet sich der Laden ab, statt still zu verstummen |
Angefasst wird nur, was dieser App gehört. Was jemand früher von Hand im Shop-Admin eingetragen hat, sieht dieses Programm gar nicht und rührt es deshalb auch nicht an — das gehört dort gelöscht, sonst kommt jede Bestellung zweimal.
Warum der API-Geheimschlüssel: Shopify unterschreibt einen Webhook, den eine App über die Schnittstelle anlegt, mit dem Geheimschlüssel der App — nicht mit dem Geheimnis, das beim Eintragen zurückkommt. Fehlt er, kommt jede Bestellung an und wird als falsche Unterschrift abgewiesen: der Shop meldet erfolgreiche Zustellung, hier entsteht nichts. Der Verbindungstest sagt es ausdrücklich, wenn er fehlt.
Das alte Webhook-Geheimnis gibt es weiterhin, und Läden, deren Webhooks noch von Hand im Admin stehen, laufen unverändert: die Annahmestelle prüft gegen beide.
Der Weg muss von außen erreichbar sein. Der Webhook kommt von Shopify und nicht aus dem Hausnetz: POST /api/v1/shopify/webhook auf der Domain, unter der dieser Server ohnehin steht. Ein eigener Server-Block ist dafür nicht nötig, ein eigener Port auch nicht — der Weg läuft durch dieselbe Weiterleitung wie die übrige Schnittstelle. Was er zusätzlich braucht: der rohe Rumpf muss unverändert ankommen. Ein Zwischenglied, das JSON neu formatiert, erzeugt andere Bytes und damit eine andere Unterschrift; die Bestellung wird dann mit gültigem Geheimnis abgewiesen, und niemand sieht, warum.
**Drücken Sie danach in der App Verbindung prüfen.** Der Test ändert nichts und meldet, was sonst erst an der ersten Bestellung auffällt: die Währung des Ladens, die tatsächlich vergebenen Berechtigungen und die Lagerorte.
Das Token liegt verschlüsselt in der Datenbank (AES-256-GCM, Schlüssel aus
APP_ENCRYPTION_KEY) — wie das TOTP-Geheimnis. Wer den Schlüssel wechselt, muss den Laden neu eintragen; bis dahin wird kein Webhook angenommen, und das ist Absicht.
Der Webhook-Endpunkt ist die einzige öffentliche Stelle dieses Servers, die Belege erzeugt. Shopify kann sich nicht anmelden — es kennt weder Benutzer noch Mandanten. Die HMAC-Unterschrift ist die ganze Tür. Ein Webhook ohne gültige Unterschrift wird verworfen, und zwar mit derselben Antwort wie einer von einem unbekannten Laden: wer probiert, welche Läden eingetragen sind, soll es nicht am Statuscode merken.
Was dann geschieht
Der Webhook legt die Bestellung nur ab und antwortet sofort. Shopify erwartet eine Antwort binnen fünf Sekunden und stellt sonst erneut zu — wer erst übersetzt und dann antwortet, bekommt bei jeder langsamen Sekunde dieselbe Bestellung ein zweites Mal.
Übersetzt wird nachts, oder auf Knopfdruck. Aus einer Bestellung wird ein Auftrag, kein Rechnung: Kunde über die E-Mail-Adresse zusammengeführt, Artikel über SKU oder Barcode, Versand als eigene Position.
Die Rückfallebene: wenn ein Webhook verloren geht
Ein Webhook ist eine Zustellung, kein Vertrag. Er geht verloren, wenn der Server gerade neu startet, wenn die Leitung klemmt, wenn Shopify nach mehreren Fehlversuchen aufgibt. In genau diesem Fall gibt es keine Fehlermeldung — nur eine Bestellung, die niemand je gesehen hat.
Deshalb fragt der Server stündlich nach, was sich im Shop geändert hat (updated_at, nicht created_at — sonst fehlt jede Bestellung, die nachträglich bezahlt wurde), und legt an, was fehlt. Der Schlüssel dagegen ist die Shopify-Bestellnummer: dieselbe Bestellung wird nie zweimal angelegt.
An jeder Bestellung steht, woher sie kam. Steht dort überwiegend abgleich, kommen die Webhooks nicht an — eine Diagnose, die sonst niemand stellt, weil am Ende ja alles da ist.
Nur Euro
Bestellungen in einer anderen Währung werden nicht übernommen. Sie landen in der Warteliste mit dem Grund. Dieses Programm rechnet in Euro, kennt keine Kurse und führt keinen Kurstag; ein Betrag in fremder Währung als Euro verbucht fällt niemandem auf.
Das ist der Grund, warum der Verbindungstest die Währung des Ladens meldet: ein Shop, der in Dollar rechnet, nimmt jede Bestellung an — und keine davon kommt an.
Was ausdrücklich nicht von selbst geschieht
Es wird kein Artikel angelegt. Eine unbekannte SKU wird gemeldet und wartet. Ein Artikel, den ein Webhook erzeugt hat, stünde ohne Preis, ohne Steuersatz und ohne Konto im Stamm — und niemand wüsste, woher er kam.
Es entsteht kein halber Auftrag. Fehlt ein Artikel, wartet die ganze Bestellung. Ein Auftrag, dem Positionen fehlen, sieht aus wie ein Auftrag und ist keiner.
Unser Steuersatz gilt, nicht der des Shops. Läuft beides auseinander, wird es gemeldet: die Rechnung stellen wir aus, und für ihre Richtigkeit haften wir. Wer im Shop 10 % einstellt und im Artikelstamm 20 %, bekommt sonst Rechnungen, die dem Kunden anders aussehen als seine Bestellung.
Eine Rechnung nur auf Wunsch. Die Einstellung ist voreingestellt aus: eine festgeschriebene Rechnung lässt sich nur noch per Storno korrigieren.
Der Lagerstand zurück in den Shop
Ebenfalls freiwillig und ebenfalls aus. Wer es einschaltet, macht dieses Programm zur Quelle der Wahrheit für den Bestand — und überschreibt damit, was jemand im Shop von Hand eingetragen hat.
Dafür braucht die App im Shop zusätzlich read_locations, read_inventory und write_inventory.
Und einen zugeordneten Lagerort. Ein Shop führt oft mehr als einen; schon ein frisch angelegter Entwicklungsladen bringt drei mit („Shop location", „My Custom Location", „Snow City Warehouse"). Solange keiner zugeordnet ist, wird nichts geschrieben. Die Zuordnung steht in der App unter Lager im Shop.
Früher nahm das Rückschreiben schlicht den ersten Lagerort. Das ist keine Zuordnung, das ist ein Würfel — und der Bestand landet im falschen Lager, ohne Fehlermeldung.
Geschrieben wird nur, was beide Seiten kennen: ein Artikel mit einer SKU, zu der es im Shop eine Variante gibt. Was der Shop nicht führt, wird übersprungen — ihn dort anzulegen wäre etwas anderes als einen Bestand zu melden.
Ein negativer Bestand geht als Null hinaus. Bei uns ist er eine Auskunft („mehr verkauft als da"), im Shop wäre er eine Einladung, noch mehr zu verkaufen.
Shopifys Schnittstelle begrenzt die Anzahl der Anfragen mit einem Eimer: bei den kleinen Tarifen fasst er 40 und läuft mit zwei je Sekunde nach, bei größeren mehr (ein Entwicklungsladen meldet 400). Das Programm rechnet deshalb mit dem Verhältnis und nicht mit einer festen Zahl. Beim Rückschreiben geht es um eine Anfrage je Artikel — bei dreihundert Artikeln also um einige Minuten. Das Programm bremst von selbst, bevor es klemmt; im Protokoll steht am Ende, wie viele geschrieben und wie viele übersprungen wurden.
Stellschrauben in der .env
Alle haben brauchbare Vorgaben; wer nichts einträgt, bekommt sie.
| Name | Vorgabe | Wofür |
|---|---|---|
MAX_FAILED_LOGINS | 10 | Ab dem wievielten Fehlversuch ein Konto gesperrt wird |
LOCKOUT_MINUTES | 15 | Die erste Sperre |
LOCKOUT_MAX_MINUTES | 1440 | Die Sperre verdoppelt sich mit jedem weiteren Fehlversuch — bis hierher. Ohne Deckel könnte jemand, der eine E-Mail-Adresse kennt, ein Konto dauerhaft lahmlegen; ein Administrator hebt die Sperre ohnehin sofort auf |
MAX_DEVICES | 10 | Wie viele Geräte je Konto gleichzeitig angemeldet sein dürfen. Beim elften fällt das älteste heraus. 0 schaltet die Grenze ab |
ACCESS_TOKEN_TTL | 900 | Lebensdauer des Zugriffstokens in Sekunden |
REFRESH_TOKEN_TTL | 2592000 | Lebensdauer des Erneuerungstokens (30 Tage). Ein gleitendes Fenster: jede Erneuerung setzt es neu |
SESSION_MAX_LIFETIME | 2592000 | Die absolute Höchstdauer einer Sitzung, gerechnet ab der Anmeldung — nicht ab der letzten Erneuerung. Ohne diesen Deckel lebt eine Sitzung, die knapp innerhalb des Fensters benutzt wird, unbegrenzt weiter. Neu in 0.1.15; bestehende Sitzungen haben die Grenze rückwirkend ab ihrem Beginn bekommen |
ENABLE_API_DOCS | aus | Die Schnittstellenbeschreibung unter /api/docs. Im Betrieb aus lassen — sie zeigt jedem die vollständige Endpunktliste |
PDF_ALLOW_NO_SANDBOX | aus | Notausgang. Chromium setzt die PDFs seit 0.1.15 mit Sandkasten; auf einem Wirt mit altem Kern kann der fehlen, und dann steht der PDF-Dienst still. Das ist die richtige Vorgabe — still ist besser als heimlich ungeschützt. Wer ihn dort braucht, schaltet ihn hiermit bewusst ab und findet die Begründung im Protokoll wieder |
Nach einer Änderung: docker compose up -d --force-recreate api.
APP_SECRETzu tauschen beendet jede laufende Sitzung. Das ist die schnellste Notbremse, wenn etwas passiert ist, und kostet nichts als eine erneute Anmeldung aller. Ausführlich indocs/SICHERHEIT.md, Abschnitt 13.
Protokolle ansehen
docker compose logs -f api # Anwendungsprotokoll
docker compose logs -f caddy-internal # Zugriffe, Zertifikate (BEHIND_PROXY)
docker compose logs -f caddy # dasselbe im Modus STANDALONE
docker compose logs -f backup # Sicherungen
Aufräumen
Docker sammelt alte Abbilder an. Auf dem Entwicklungsrechner läuft dafür eine wöchentliche geplante Aufgabe unter dem Konto SYSTEM, die scripts/aufraeumen.ps1 startet. Sie entfernt Abbilder, die älter als eine Woche und unbenutzt sind, und greift zusätzlich, sobald mehr als 10 GB belegt sind — die reine Altersregel feuert bei täglichen Bauten nämlich nie.
Von Hand, monatlich, genügt auf einem reinen Server:
docker system prune -f
Unter Windows liegen die Abbilder in einer VHDX-Datei auf C:, nicht im
DATA_DIR. Diese Datei schrumpft nicht, wenn Abbilder gelöscht werden — sie gibt den Platz nur intern frei. Wer die Datei wirklich verkleinern will, brauchtOptimize-VHDoder ein Zurücksetzen von Docker Desktop.
Umzug auf einen anderen Rechner
- Auf dem alten Server:
docker compose down, dannDATA_DIRund.envauf den neuen Rechner kopieren. - Auf dem neuen Rechner: Docker installieren, Quellcode holen,
.envhineinkopieren, denDATA_DIR-Pfad darin anpassen,docker compose up -d. - DynDNS bzw. Portfreigabe auf den neuen Rechner umstellen.
- Die Apps merken davon nichts, die Serveradresse bleibt gleich.
Dafür gibt es zwei Skripte, die den Vorgang samt Integritätsprüfung übernehmen:
./scripts/migrate-export.sh [zielverzeichnis] # auf dem alten Rechner
./scripts/migrate-import.sh <archiv> # auf dem neuen
Unter Windows heißen sie migrate-export.ps1 und migrate-import.ps1.
8a. Das Zustellungsprotokoll der Webhooks
Jede eingehende Webhook-Anfrage bekommt eine Zeile — die angenommene, die abgewiesene und die von einem unbekannten Laden.
Warum das nötig war. Dreimal stand hier die Frage im Raum, ob ein Webhook angekommen ist, und dreimal war sie nicht beantwortbar. Eine angenommene Zustellung hinterlässt eine Spur (lastEventAt, die Webhook-Kennung am Beleg); eine abgewiesene hinterließ nur eine Zeile im Behälterprotokoll — und die ist beim nächsten Neustart weg. „Nie angekommen" und „angekommen und abgewiesen" sahen von außen gleich aus, obwohl das eine ein Netzproblem ist und das andere ein falscher Schlüssel.
Zu sehen sind die letzten zehn im Verbindungstest (Einstellungen → Onlineshop → Verbindung testen). Eine leere Liste heißt: es kam nie etwas an.
Was in einer Zeile steht: Zeitpunkt, Thema, Laden, ob die Unterschrift stimmte, was daraus wurde (ANGENOMMEN, ABGEWIESEN, UNBEKANNT, FEHLER) und der Grund. Kein Rumpf — die Bestellung selbst steht ohnehin in der Bestellliste, und ein Protokoll mit Kundendaten wäre eine zweite Ablage personenbezogener Daten ohne Zweck.
Aufbewahrung 90 Tage; die nächtliche Wartung räumt Ältere weg. Länger wäre eine Sammlung fremder Anfragedaten ohne Zweck, kürzer hilft nicht bei einer Frage, die erst nach zwei Wochen gestellt wird.
Nach außen ändert das Protokoll nichts: Shopify bekommt weiterhin 200, und ein unbekannter Laden erfährt weiterhin nicht, dass er unbekannt ist.
8b. Die Ausgabenseite: was der Betreiber davon wissen muss
Der Anwender findet die Ausgaben in seiner Anleitung, Kapitel 9a und 9b. Hier steht, was beim Betrieb daran hängt.
Der Nummernkreis der Ausgabenbelege
Seit 0.2.0 gibt es einen zwölften Nummernkreis: EXPENSE_RECEIPT, Vorgabe ER-{YYYY}-{SEQ}. Er ist der einzige für etwas, das hereinkommt statt hinauszugehen — die Belegsammlung nach § 132 BAO braucht eine eigene Ordnung, die der Lieferant nicht liefert.
Er verhält sich wie die anderen: Zeilensperre beim Ziehen, Jahreszähler, änderbares Muster unter Einstellungen → Nummernkreise. Bei einer bestehenden Installation legt ihn die Migration mit an; wer ihn vorher schon von Hand angelegt hatte, behält seinen.
Die Belege selbst
Ein angehängter Beleg liegt wie jede andere Datei im Dateispeicher unter data/files und wird von restic mitgesichert. Die Aufbewahrungsfrist steht an der Datei (retainUntil): sieben Jahre ab Ende des Kalenderjahres, so wie es § 132 BAO verlangt. Die Aufräumroutine rührt sie bis dahin nicht an — auch dann nicht, wenn der Datensatz daneben verworfen wurde.
Die Zuordnung auf die Kennzahlen der Beilage E 1a
Der Jahresreport sortiert nach den Kennzahlen des Formulars. Die Zuordnung steht als eine Quelle im Code (server/src/steuer/e1a.ts); hier ist sie zum Nachschlagen. Bezeichnungen wörtlich aus dem Formular E 1a-K, Version vom 07.02.2025 (Beilage zur Einkommensteuererklärung E 1 für Kleinbetriebe für 2024, formulare.bmf.gv.at).
| KZ | Bezeichnung laut Formular | Ausgabengruppen des Programms |
|---|---|---|
| 9040 | Betriebseinnahmen (Waren-/Leistungserlöse) | die bezahlten Ausgangsrechnungen |
| 9100 | Waren, Rohstoffe, Hilfsstoffe | Wareneinkauf |
| 9110 | Beigestelltes Personal (Fremdpersonal) und Fremdleistungen | (noch keine eigene Gruppe — Fremdleistungen liegen als Kategorie unter Wareneinkauf) |
| 9120 | Ausgaben für eigenes Personal | Personal |
| 9130 | Abschreibungen auf das Anlagevermögen | Abschreibung, Geringwertige Wirtschaftsgüter |
| 9160 | Reise- und Fahrtspesen inkl. Km-Geld und Diäten | Reise |
| 9180 | Ausgaben für Miete, Pacht, Leasing | Miete |
| 9220 | Zinsen und ähnliche Aufwendungen | Zinsen |
| 9225 | Eigene Pflichtversicherungsbeiträge, Beiträge zu Versorgungs- und Unterstützungseinrichtungen und Beiträge zur Selbständigenvorsorge | Sozialversicherung |
| 9230 | Übrige, in den obigen Kennzahlen nicht erfasste, Betriebsausgaben | Energie, Instandhaltung, Kfz-Kosten, Werbung, Büromaterial, Telefon/Internet, Versicherungen, Beiträge, Rechts- und Beratungskosten, Sonstiges |
Warum so viele Gruppen auf 9230 zeigen: das Formular hat weniger Zeilen als ein Betrieb Ausgabenarten. Im Programm bleiben sie getrennt, weil der Betrieb sie getrennt sehen will; im Formular landen sie in einer Summe. Der Jahresreport zeigt unter der Sammelzeile, woraus sie besteht.
Was das Programm bewusst nicht ausrechnet: Gewinnfreibetrag (9221/9227), Investitionsfreibetrag (9276/9277), die Arbeitsplatzpauschalen (9215/9217), Öko-Zuschlag (9338), Spenden (9243 ff.). Nichts davon ist aus Buchungen ablesbar. Wer es automatisch ausrechnen ließe, gäbe eine Zahl aus, für die er nicht geradestehen kann.
Wenn sich das Formular ändert, ist e1a.ts die einzige Stelle. Der Compiler meldet sich, wenn eine neue Ausgabengruppe ohne Kennzahl dazukommt.
Die Übernahme aus einer Tabelle
POST /incoming-invoices/uebernahme nimmt den Inhalt einer CSV-Datei als Zeichenkette entgegen — zweistufig: ohne schreiben wird nur geprüft. Die Grenze liegt bei zwei Megabyte Text, was für einige zehntausend Zeilen reicht; sie ist keine eigene Upload-Strecke, sondern der normale JSON-Rumpf.
Angelegt werden Entwürfe. Sie zählen nirgends mit, bis jemand sie freigibt — das ist Absicht: eine übernommene Zeile hat keine Steuerangaben und keinen Beleg, und beides soll ein Mensch ansehen, bevor es in die Einnahmen-Ausgaben-Rechnung geht.
8c. Die Kasse
Ab 0.3.0 kann vor Ort kassiert werden — Kurse, Startgelder, Kleinverkauf. Der Bereich heißt Kasse und steht auf dem Telefon an zweiter Stelle in der unteren Leiste.
Kassiert wird am Telefon, ausgewertet am Rechner. Auf Windows zeigt der Bereich den Tagesabschluss und den Weg zu den Einstellungen, aber keinen Verkauf. Das hat zwei Gründe: kassiert wird dort, wo der Kunde steht, und zwei Geräte, die gleichzeitig kassieren, teilen sich eine Belegnummernfolge, die lückenlos bleiben muss.
Ohne Verbindung wird nicht kassiert. Alles andere in der App arbeitet offline weiter; die Kasse nicht. Die Belegnummer zieht der Server, und eine Lücke darin lässt sich nachträglich nicht heilen. Der Kassenbildschirm sagt das deutlich, statt eine Sicherheit vorzutäuschen, die er nicht hat.
Der Punkt, an dem sich Betriebe verrechnen
Eine Bankomat- oder Kreditkartenzahlung vor Ort ist ein Barumsatz (§ 131b Abs 1 Z 3 BAO), auch wenn kein Schein den Besitzer wechselt. „Wir nehmen ja kaum Bargeld" stimmt in vielen Betrieben — und die Kartenumsätze daneben reißen die Grenze trotzdem.
Eine Überweisung ist dagegen kein Barumsatz. Deshalb steht „Auf Rechnung" auf dem Kassenbildschirm immer zur Verfügung, auch wenn bar und Karte gesperrt sind.
Die zwei Zustände, und wer sie wählt
Bevor bar oder mit Karte kassiert werden kann, muss der Betrieb eine Kassenpflicht-Erklärung abgeben. Sie steht unter Einstellungen → Firmenstammdaten → Kasse und darf nur ein Administrator setzen.
a) Unter den Grenzen
Jahresumsatz unter 15.000 €, davon unter 7.500 € bar und mit Karte vor Ort.
Dann darf ohne Signatureinheit kassiert werden. Das ist keine Notlösung und kein Testbetrieb: der Beleg ist ein vollwertiger Beleg und trägt keinen Vermerk.
Was trotzdem gilt: die Belegerteilungspflicht nach § 132a BAO. Sie hängt an keiner Umsatzgrenze. Wer einen Barumsatz macht, hat einen Beleg zu erteilen — auch der kleinste Betrieb, auch ohne Registrierkasse. SIMPro erzeugt ihn deshalb bei jedem Weg.
b) Registrierkassenpflichtig – Signatur eingerichtet
Setzt eine vollständig eingerichtete Registrierkasse voraus: fiskaltrust-Konto, FinanzOnline-Webservice-Benutzer, CashBox, geprüfte Verbindung, Startbeleg und dessen Prüfung mit dem Belegcheck. Dieser Zustand lässt sich nicht einfach anklicken — er behauptet zwei Dinge auf einmal, und das zweite kann niemand erklären. Er wird frei, wenn der Einrichtungsassistent alle Schritte grün meldet (kommt mit Stufe 2).
Der Wechsel a → b ist jederzeit möglich. Der Weg b → a verlangt eine Begründung, die dauerhaft im Prüfprotokoll steht: die Registrierkassenpflicht endet nach § 131b Abs 4 BAO erst, wenn die Grenzen in einem Folgejahr unterschritten werden und ein neuerliches Überschreiten nicht zu erwarten ist. Die zweite Hälfte ist eine Prognose.
Was SIMPro mitrechnet — und was es nicht kann
Auf dem Kassenbildschirm und in den Einstellungen stehen beide Werte mit ihrer Ausschöpfung. Ab 80 % bei beiden kommt eine Warnung, in der App und als Meldung an die Administratoren. Bei nur einem der beiden kommt keine: sonst warnte das Programm jeden Betrieb mit ordentlichem Umsatz und ohne einen einzigen Bargeschäftsfall, und nach dem dritten Mal sieht niemand mehr hin.
Gerechnet wird aus den eigenen Belegen:
- Jahresumsatz netto, aus den festgeschriebenen, nicht stornierten Ausgangsbelegen.
- Barumsatz aus den Bar- und Kartenzahlungen, nach Zahlungsdatum, umgerechnet auf den Nettoanteil des jeweiligen Belegs.
Die Einschätzung bleibt beim Betrieb. Diese Rechnung ist nur vollständig, wenn aller Umsatz durch dieses Programm läuft. Ein zweiter Betrieb desselben Unternehmers zählt getrennt; Umsätze, die nie hier erfasst wurden, fehlen; ein abweichendes Wirtschaftsjahr verschiebt den Zeitraum. Im Zweifel entscheidet die Steuerberatung, nicht der Balken auf dem Bildschirm.
Der Stichtag
Werden beide Grenzen in einem Quartal überschritten, beginnt die Pflicht nicht sofort. Sie beginnt mit dem viertfolgenden Monat nach Ablauf des Quartals — vier Monate Zeit, eine Kasse einzurichten:
| Grenzen gerissen im | Pflicht ab |
|---|---|
| 1. Quartal (Jän–Mär) | 1. Juli desselben Jahres |
| 2. Quartal (Apr–Jun) | 1. Oktober desselben Jahres |
| 3. Quartal (Jul–Sep) | 1. Jänner des Folgejahres |
| 4. Quartal (Okt–Dez) | 1. April des Folgejahres |
Der häufigste Rechenfehler ist, die vier Monate ab dem Tag des Überschreitens zu zählen statt ab dem Quartalsende. Bei einem Überschreiten im Dezember landet man dann im April — statt im April des nächsten Jahres.
SIMPro zeigt den Stichtag, sobald er feststeht, und ab diesem Tag sind Bar- und Kartenzahlung gesperrt, bis Zustand b eingerichtet ist. Das ist streng, und mit Absicht: ein unsignierter Barumsatz nach dem Stichtag ist eine Finanzordnungswidrigkeit (§ 51 FinStrG), und der Betrieb merkt es erst bei der Prüfung. „Auf Rechnung" bleibt offen.
Das Prüfprotokoll
Jede Entscheidung hängt sich als Zeile an und wird nie überschrieben — mit Datum, Person, Begründung und den Umsatzzahlen, die zu diesem Zeitpunkt galten. Die Datenbank lässt auf dieser Tabelle nur Lesen und Anhängen zu, kein Ändern und kein Löschen.
Wer in drei Jahren gefragt wird, seit wann signiert kassiert wird und was vorher galt, findet die Antwort dort.
Kassenartikel einrichten
Unter Einstellungen → Kassenartikel. Ein Artikelstamm mit vierhundert Positionen ist eine Liste zum Suchen; die Kasse braucht die Auswahl daraus — zehn bis zwanzig sind eine gute Zahl, in der Reihenfolge, in der sie gebraucht werden.
Je Kachel lassen sich einstellen:
- Reihenfolge durch Ziehen am Griff rechts.
- Farbe aus neun kräftig verschiedenen Tönen. Farbe ist an einer Kasse kein Schmuck: wer zwanzig Mal in der Stunde dieselbe Kachel trifft, trifft sie über die Farbe, nicht über den Text.
- Aufschrift, wenn der Artikelname nicht auf eine Kachel passt. „Tageskarte" passt, „Tageskarte Erwachsene inkl. Leihausrüstung" nicht. Ohne eigene Aufschrift steht der Artikelname da und folgt ihm auch, wenn er im Stamm umbenannt wird.
Die Kachel Freier Betrag steht immer vorn und braucht keine Einrichtung. Sie fragt nach Text, Betrag und Steuersatz. Der Text landet auf dem Beleg — auf ihm muss stehen, wofür gezahlt wurde (§ 11 Abs 1 Z 3 UStG); „Diverses" ist bei einer Prüfung keine Bezeichnung.
Kassieren
Kacheln antippen, dann unten die Zahlungsart. Dreimal dieselbe Kachel ergibt drei Stück und keine drei Zeilen.
- Bar öffnet den Rückgeldrechner. Oben steht der Betrag, darunter die Tasten für das, was ein Mensch hinlegt — bei 17,30 € sind das passend, 18,00, 20,00, 50,00 und 100,00. Das Rückgeld steht darunter, nach dem Abschluss zusätzlich mit der Stückelung („1× 2,00 € · 1× 1,00 € · …"). Eine Unterzahlung nimmt die Kasse nicht an. Teilzahlungen kennt sie nicht; für einen Rest gibt es „Auf Rechnung".
- Karte zeigt den Betrag zum Abgleich mit dem Terminal und nimmt die Referenz des Terminals auf — freiwillig, aber sie hilft beim Zuordnen der Abrechnung. Ab Stufe 3 übernimmt das die Zahlungs-App auf demselben Gerät.
- Auf Rechnung braucht einen Kunden: ein Laufkunde hat keine Anschrift, und ohne die ist eine Rechnung nach § 11 UStG unvollständig. Es entsteht eine Rechnung im Rechnungskreis, kein Kassenbeleg — sie ist offen und nicht bezahlt, denn hier ist kein Geld geflossen.
Ein Verkauf mit Bar oder Karte ergibt einen Kassenbeleg im eigenen Nummernkreis KB-JJJJ-NNNN. Er ist sofort bezahlt und hat keine Fälligkeit.
Der Warenkorb bleibt stehen, wenn etwas schiefgeht. Eine Kasse, die bei einem Fehler den Korb wegwirft, lässt jemanden zwanzig Positionen neu tippen.
Eine offene Rechnung vor Ort kassieren
Über den Beleg selbst: eine Rechnung, die jemand am Tresen bar bezahlt, ist ein Barumsatz — es gilt dieselbe Freigabe wie beim Verkauf. Es entsteht kein neuer Beleg: die Rechnung ist schon geschrieben, hier fließt nur das Geld.
Der Tagesabschluss
Über das Symbol rechts oben im Kassenbereich, auch als PDF.
Er ist gebaut, um neben der Kassenlade zu liegen: der Barbetrag steht groß und allein oben. Er ist die einzige Zahl, die mit dem Inhalt der Lade zu vergleichen ist. Kartenumsätze kommen vom Anbieter aufs Konto und stehen daneben, nicht darin — wer beides zusammenzählt und dann nachzählt, findet jeden Abend eine Differenz, die es nicht gibt. Auf dem Ausdruck gibt es Felder für das Zählergebnis, die Differenz und ein Handzeichen.
Gezählt wird nach dem Zahlungsdatum, nicht nach dem Belegdatum: eine Rechnung vom Vormonat, die heute bar bezahlt wird, liegt heute in der Lade.
Der Testmodus
Nur für den Entwicklungsstapel. Er erlaubt Kassieren, wo sonst gar nicht kassiert werden könnte, und stempelt jeden Beleg mit „TESTBELEG – KEINE RKSV-SIGNATUR". Ein solcher Beleg zählt in keiner Summe mit: nicht im Tagesabschluss, nicht in den Umsatzgrenzen, nicht in der E/A-Rechnung. Im Tagesabschluss steht er trotzdem, in einer eigenen Zeile darunter — ihn zu verschweigen wäre die andere Hälfte desselben Fehlers.
Einschalten verlangt zwei Dinge:
KASSE_TESTMODUS=1in der.env, und- ein selbst gebautes Abbild. Das veröffentlichte trägt eine Markierung aus dem Bauschritt und weist den Modus ab.
# Nur im Entwicklungsstapel:
echo 'KASSE_TESTMODUS=1' >> .env
docker compose build api
API_IMAGE=rechnung-api:local docker compose up -d api
Der Riegel ist nicht absolut, und er soll nicht so klingen. Wer root auf seinem eigenen Server hat, kommt an jedem Riegel vorbei. Er verhindert das Versehen, nicht die Absicht. Die eigentliche Sicherung ist der Stempel am Beleg: er steht in der Datenbank und lässt sich nachträglich nicht wegschalten. Ein Betrieb, der sich als registrierkassenpflichtig erklärt hat, bekommt den Testmodus ohnehin nicht — auch nicht mit gesetzter Variable.
Was noch nicht da ist
| Signatur nach RKSV | Seit 0.3.1 da — siehe Kapitel 8d. Erst wenn der Einrichtungsassistent alle sechs Schritte grün meldet, lässt sich Zustand b wählen. |
| Kartenzahlung über das Gerät | Stufe 3 (Terminal-App) und 4 (Tap to Pay). Bis dahin: am Terminal zahlen, in der Kasse bestätigen. |
| Bondruck über Bluetooth | Später. Die Belegerteilungspflicht hängt nicht am Papier — ein Beleg per E-Mail, QR oder PDF erfüllt sie ebenso. |
| Trinkgeld, Kassenlade, Schichten, Teilzahlung | Nicht gebaut. Sie vorher zu bauen hieße, sie zu raten. |
8d. Die Registrierkasse einrichten (RKSV)
Ab 0.3.1 kann SIMPro Barumsätze signieren — nach der Registrierkassensicherheitsverordnung, über die fiskaltrust.Middleware.
Wer das braucht: ein Betrieb, der die Umsatzgrenzen des § 131b BAO überschreitet (Kapitel 8c). Wer darunter liegt, kassiert ohne Signatureinheit weiter — das ist zulässig, und dieses ganze Kapitel geht ihn nichts an.
Der Weg dorthin sind sechs Schritte. SIMPro führt sie unter Einstellungen → Registrierkasse als Liste und lässt immer nur den nächsten offen. Drei davon passieren woanders, und die hakt ein Mensch ab: das Programm kann nicht nachsehen, ob bei fiskaltrust ein Konto besteht.
Vorweg: was das kostet und was es voraussetzt
| fiskaltrust-Konto | Für den Sandkasten kostenlos. Für den Produktivbetrieb ein Vertrag — dahinter steht eine echte Signaturerstellungseinheit, und die kostet Geld. |
| FinanzOnline | Ein Webservice-Benutzer. Kostenlos, aber er muss angelegt werden. |
| Ein Smartphone | Für den Belegcheck. Die App „BMF Belegcheck" gibt es für Android und iOS. |
| Der Container | Die Middleware läuft im Stapel mit, im Profil kasse. Sie braucht rund 300 MB Arbeitsspeicher. |
Schritt 1 — das fiskaltrust-Konto
- Auf portal-sandbox.fiskaltrust.at registrieren (Sandkasten) oder auf portal.fiskaltrust.at (Produktion).
- Als Land Österreich wählen.
- Die Rolle PosDealer beantragen, wenn danach gefragt wird.
Danach in SIMPro den Schritt abhaken.
Sandkasten und Produktion sind getrennte Systeme. Eine dort angelegte CashBox ist hier nicht verwendbar und umgekehrt. Der Wechsel ist deshalb kein Umschalten einer Einstellung, sondern eine neue CashBox mit einem neuen Startbeleg — und in SIMPro eine neue Kasse. Die alte bleibt sichtbar, damit die Testbelege nachvollziehbar bleiben, erzeugt aber nichts mehr.
Schritt 2 — der FinanzOnline-Webservice-Benutzer
Die Registrierkasse meldet sich selbst bei FinanzOnline an. Dafür braucht sie einen eigenen Zugang — nicht den, mit dem ein Mensch sich anmeldet.
In FinanzOnline:
- Admin → Benutzer → Benutzer anlegen
- Als Typ Webservice-Benutzer wählen
- Benutzer-Identifikation und PIN vergeben und notieren — die PIN ist danach nicht mehr abrufbar
- Dem Benutzer das Recht Registrierkassen (RK) geben
Diese drei Angaben (Teilnehmer-Identifikation, Benutzer-Identifikation, PIN) dann im fiskaltrust-Portal hinterlegen: Konfiguration → FinanzOnline.
Danach in SIMPro den Schritt abhaken.
Sie gehören in
B:\Rechnung-privat\oder in den Passwortmanager des Betreibers, nicht ins Repository und nicht in eine E-Mail.
Schritt 3 — die CashBox
Im fiskaltrust-Portal:
- Konfiguration → CashBox → Neue CashBox
- Eine Queue für Österreich hinzufügen, Speicherart SQLite
- Eine Signaturerstellungseinheit hinzufügen — im Sandkasten das Demo-Zertifikat, in der Produktion die vertraglich vereinbarte
- Eine Kassenidentifikationsnummer vergeben. Sie steht später auf jedem Beleg; etwas Sprechendes ist besser als eine Zufallsnummer.
- Speichern und neu erstellen drücken
Der Schritt, der am ehesten vergessen wird, ist der fünfte. Queue und Signatureinheit können angelegt sein und trotzdem nicht in der Konfiguration der CashBox stehen — das Portal erzeugt sie erst mit „Speichern und neu erstellen". Vorher lädt die Middleware eine leere Konfiguration, startet, meldet „Started all packages" — und horcht auf keinem Port. In SIMPro sieht das aus wie „nicht erreichbar", und man sucht beim Netzwerk statt im Portal.
Nachsehen lässt es sich:
DATA_DIR/fiskaltrust/service/Configuration-*.json. Stehen dort"ftQueues":[]und"ftSignaturCreationDevices":[], ist genau das passiert. Im Portal neu erstellen, danndocker compose --profile kasse up -d --force-recreate fiskaltrust.
Auf derselben Seite stehen danach CashBoxId und AccessToken. Beide werden gleich gebraucht.
Das AccessToken ist ein Passwort. Nicht weitergeben.
Danach in SIMPro den Schritt abhaken.
Schritt 4 — Zugangsdaten eintragen und Verbindung prüfen
Zuerst die Middleware starten. In der .env des Stapels:
FT_CASHBOX_ID=<die CashBoxId aus Schritt 3>
FT_ACCESS_TOKEN=<das AccessToken aus Schritt 3>
FT_SANDBOX=1 # 0 für die Produktion
Dann:
docker compose --profile kasse up -d fiskaltrust
docker compose logs -f fiskaltrust
Beim Start sagt der Container, welche CashBox er bedient und ob er im Sandkasten läuft. Fehlen die Zugangsdaten, startet er gar nicht erst und sagt warum — das ist besser, als zu laufen und bei jedem Beleg zu scheitern.
Danach in SIMPro unter Einstellungen → Betrieb → Registrierkasse → Kasse anlegen:
| Feld | Was hinein gehört |
|---|---|
| Name | Wofür die Kasse steht, z. B. „Kasse Ladengeschäft" |
| Kassenidentifikationsnummer | Die aus Schritt 3 |
| Einheit | fiskaltrust.Middleware |
| Umgebung | Sandkasten oder Produktion |
| Adresse | http://fiskaltrust:1200 — so heißt der Dienst im Stapel |
| CashBoxId | Aus Schritt 3 |
| AccessToken | Aus Schritt 3. Es wird verschlüsselt abgelegt und kommt nie wieder heraus |
Dann auf Prüfen. SIMPro spricht die Middleware an und fragt sie nach ihrem Zustand. Erst wenn sie antwortet, geht es weiter.
Warum das Feld „Einheit" auch einen Nachbau anbietet: für Vorführungen und für die Testreihe. Ein Nachbau erzeugt keine gültigen Signaturen; SIMPro sagt das im Verbindungstest und legt einen roten Streifen über den Bildschirm, solange er eingestellt ist. In einem Produktivbetrieb hat er nichts verloren.
Schritt 5 — der Startbeleg
Ein Knopfdruck. Der Startbeleg ist der erste Beleg der Kasse, über null Euro, und er eröffnet die Belegkette (§ 6 RKSV).
Nur einmal. Ein zweiter wäre ein zweiter Anfang derselben Kette; SIMPro lässt ihn nicht zu.
Schritt 6 — der Belegcheck
Den QR-Code des Startbelegs mit der App BMF Belegcheck scannen. Die App sagt, ob die Signatur gültig ist und ob die Kasse bei FinanzOnline registriert ist.
Das Ergebnis dann in SIMPro bestätigen. Damit ist die Kasse aktiv, und jeder Barumsatz wird ab sofort signiert.
Dieser Schritt ist der einzige, den weder SIMPro noch fiskaltrust für den Betrieb tun können. Er ist auch der, mit dem die Finanzverwaltung die Kasse als in Betrieb genommen sieht.
Was danach zu tun ist
| Wann | Was | Wo |
|---|---|---|
| Monatsende | Monatsbeleg — ein Nullbeleg zum Abschluss des Monats | Einstellungen → Betrieb → Registrierkasse |
| Jahresende | Jahresbeleg — der Monatsbeleg des Dezember. Er ist gesondert aufzubewahren und bis zum 15. Februar des Folgejahres zu prüfen | dieselbe Stelle |
| Bei Bedarf | Nullbeleg — prüft die Signatureinheit, ohne Umsatz | dieselbe Stelle |
| Einmal im Quartal | Das Datenerfassungsprotokoll sichern | Einstellungen → Betrieb → Registrierkasse → DEP |
Wenn die Sicherheitseinrichtung ausfällt
Die Kasse läuft weiter. Das ist kein Notbehelf, sondern in § 17 RKSV ausdrücklich vorgesehen:
- Der Beleg entsteht, ist gültig und trägt statt des QR-Codes den Vermerk „Sicherheitseinrichtung ausgefallen".
- SIMPro merkt sich jeden dieser Belege und signiert sie nach, sobald die Einheit wieder antwortet — in der Reihenfolge, in der sie entstanden sind.
- Auf dem Bildschirm steht ein Streifen mit der Dauer des Ausfalls.
Ab 48 Stunden ist der Ausfall über FinanzOnline zu melden. Das kann kein Programm für den Betrieb tun; SIMPro erinnert daran, mehr nicht.
Nachsignieren geschieht von selbst beim nächsten gelungenen Beleg — oder über den Knopf Jetzt nachsignieren.
Wo das Datenerfassungsprotokoll liegt
Im Datenträger der Middleware, unter DATA_DIR/fiskaltrust. Es ist sieben Jahre aufzubewahren (§ 132 BAO), und weil der Pfad unter DATA_DIR liegt, nimmt die nächtliche Sicherung es ohnehin mit.
Geht dieses Verzeichnis verloren, ist das Protokoll weg — und mit ihm der Nachweis über jeden Barumsatz seit dem Startbeleg. Vor jedem Umzug des Servers also zuerst prüfen, dass es in der Sicherung ist.
Was diese Fassung noch nicht kann
| Der Launcher ist ein Release Candidate | fiskaltrust veröffentlicht den Launcher v2 noch als rc. Der Container heftet eine datierte Fassung fest; vor dem Produktivbetrieb ist mit fiskaltrust zu klären, welche sie stützen. |
| Kein offizielles Docker-Abbild | Es gibt keines. SIMPro baut den Container selbst aus dem veröffentlichten Zip-Paket. |
| Außerbetriebnahme | Der Belegtyp ist vorbereitet, der Knopf dafür fehlt noch. Wer eine Kasse stilllegt, macht das vorerst im fiskaltrust-Portal. |
8e. Kartenzahlung einrichten
Ab 0.3.2 kann SIMPro die Kartenzahlung selbst auslösen, statt sie nur zu verbuchen. Der Weg führt über Einstellungen → Kartenzahlung; die Wahl gilt für den ganzen Betrieb.
Vier Wege, und für die meisten reicht der erste
| Einstellung | Wofür |
|---|---|
| Terminal ohne Kopplung | Jedes beliebige Bankomat-Terminal. Der Betrag wird dort getippt und in SIMPro bestätigt. Keine Einrichtung, kein Vertrag, keine App |
| Viva.com Terminal | Die Viva-App auf demselben Android-Gerät. SIMPro übergibt den Betrag und bekommt das Ergebnis zurück |
| Stripe Tap to Pay | Karte direkt ans Telefon. Gebaut, aber gesperrt — siehe unten |
| Nachbau | Bucht nichts ab. Vorführungen und Schulungen |
Der erste Weg hat einen Preis, den man kennen sollte: der Betrag wird zweimal getippt, einmal am Terminal und einmal in SIMPro. Ein Vertipper fällt niemandem auf, und abends stimmt die Abrechnung nicht. Wer viel mit Karte kassiert, fährt mit einer Kopplung besser.
Viva.com Terminal
Voraussetzungen:
- Ein Viva-Händlerkonto und die App Viva.com Terminal (
com.vivawallet.spoc.payapp) auf demselben Android-Gerät, installiert und angemeldet. - Die Anwendungskennung (
appId), die Viva für die aufrufende Anwendung vergibt. Ohne sie bleibt der Weg gesperrt, und SIMPro sagt das. - Wahlweise ein Quellcode (
sourceCode). Er trennt in der Viva-Abrechnung die Umsätze dieser Kasse von anderen.
Beide Werte in die Felder eintragen und speichern. Beim nächsten „Karte" öffnet sich die Viva-App mit dem Betrag darin.
Der Beleg des Terminals bleibt aus. SIMPro schaltet ihn ab (
show_receipt=false), damit der Kunde nicht zwei Zettel für einen Umsatz bekommt. Der Beleg nach § 132a BAO ist der von SIMPro; der des Terminals ist ein Zahlungsnachweis. Wer den Terminalbeleg trotzdem will, schaltet ihn in den Einstellungen wieder ein.
Stripe Tap to Pay: sichtbar, aber gesperrt
Der Eintrag steht in den Einstellungen und lässt sich nicht wählen. Darunter steht, warum. Das ist Absicht: ein verstecktes Verfahren sucht niemand, und eine App, die beim Versuch abstürzt, ist die schlechteste aller Antworten.
Freigeschaltet wird über die Server-Einstellung je Mandant — ohne neuen App-Bau. Die Prüfliste dazu steht in Kapitel 8f.
Wenn eine Zahlung nicht zurückkommt
Der Fall, für den der ganze Aufbau da ist: die Anbieter-App stürzt ab oder wird weggewischt, während die Zahlung läuft. SIMPro tritt in dem Moment in den Hintergrund und kann vom Betriebssystem beendet werden — auf einem Telefon mit wenig Speicher ist das kein Sonderfall.
SIMPro rät dann nicht. Es entsteht kein Beleg. Stattdessen bleibt die Zahlung als offen stehen, wird nach fünf Minuten als unklar geführt und erscheint als roter Streifen ganz oben am Kassenbildschirm. Jemand sieht in der Anbieter-App nach, wählt Bezahlt oder Nicht bezahlt und schreibt dazu, woran er es festgemacht hat. Das geht mit Namen und Zeit ins Prüfprotokoll.
Ein Programm, das an dieser Stelle selbst entscheidet, bucht im Zweifel Geld, das nie geflossen ist — oder verliert welches, das geflossen ist. Beides ist teurer als eine Frage.
Was SIMPro von der Karte sieht
Nur, was der Anbieter zurückschickt: die maskierte Kartennummer (515876******4490), die Kartenart, die Referenz und die Autorisierungsnummer. Keine vollständige Kartennummer, keine PIN, keine Prüfziffer. Die Karte wird vom Terminal oder von der Anbieter-App gelesen, nie von SIMPro — das ist auch der Grund, warum für den Weg über die Anbieter-App kein PCI-Fragebogen anfällt.
Karte vor Ort bleibt ein Barumsatz
Die Kartenzahlung ändert nichts an Kapitel 8c: nach § 131b BAO zählt eine Zahlung mit Karte am Ort der Leistung wie Bargeld. Sie geht in die Grenze von 7.500 € ein und braucht ab den Umsatzgrenzen dieselbe Signatur. Ein Betrieb, der auf Karte umstellt, um „unter der Bargeldgrenze zu bleiben", hat sich verrechnet.
8f. Tap to Pay freischalten
Ab 0.3.3 ist der Weg gebaut: die Karte wird direkt an das Telefon gehalten, ohne zweites Gerät. Er ist gesperrt ausgeliefert und wird je Mandant freigeschaltet — ohne neuen App-Bau, aber auch nicht mit einem einzigen Häkchen. Vier Dinge müssen zusammenkommen, und drei davon liegen außerhalb von SIMPro.
Die Prüfliste
| # | Was | Wer | Woran man sieht, dass es fehlt |
|---|---|---|---|
| 1 | Die App kommt aus dem Play Store | „Diese App wurde nicht aus dem Play Store installiert." | |
| 2 | Stripe-Händlerkonto, für Terminal freigegeben | Stripe | „Das hinterlegte Stripe-Händlerkonto ist für Tap to Pay nicht freigegeben." |
| 3 | Das Gerät kann es | das Gerät | „Dieses Gerät unterstützt Tap to Pay nicht." |
| 4 | Schlüssel und Standort hinterlegt | Sie | Die drei Häkchen in den Einstellungen sind nicht alle grün |
1. Aus dem Play Store, nicht seitlich aufgespielt
Google prüft das über Play Integrity. Eine APK, die von der Release-Seite heruntergeladen und von Hand installiert wurde, besteht die Prüfung nicht — und zwar dauerhaft, nicht bis zur nächsten Aktualisierung.
Das ist der Punkt, an dem die meisten hängenbleiben. SIMPro wird heute als APK ausgeliefert, nicht über den Play Store. Wer Tap to Pay will, braucht die App aus dem Store; solange es sie dort nicht gibt, bleibt der Weg über die Anbieter-App (Kapitel 8e) der praktikable.
SIMPro stürzt deswegen nicht ab und lässt sich auch nicht überreden: der Bildschirm sagt den Satz und bietet den Weg über „bestätigen" an.
2. Das Stripe-Konto
- Ein Stripe-Konto mit freigeschaltetem Terminal. Tap to Pay ist nicht in jedem Land verfügbar; Österreich ist dabei, aber die Freigabe je Konto erteilt Stripe, nicht SIMPro.
- Ein Terminal-Standort (Stripe-Portal → Terminal → Standorte). Ohne ihn verbindet sich der Leser nicht. Der Verbindungstest in den Einstellungen listet die vorhandenen auf und übernimmt einen per Antippen.
- Der geheime Schlüssel (
sk_…), nicht der veröffentlichbare (pk_…). SIMPro legt ihn verschlüsselt ab und gibt ihn nie wieder heraus.
Testmodus erkennen. Ein Schlüssel mit
sk_test_…bucht nichts ab. Die Einstellungen sagen es in Rot dazu, und der Verbindungstest wiederholt es. Wer im Testmodus kassiert und es für echt hält, hat Belege über Geld, das nie kam.
3. Das Gerät
- NFC, eingeschaltet.
- Android 8.0 oder neuer — seit 0.3.3 die Untergrenze der ganzen App.
- Kein verändertes Betriebssystem. Ein gerootetes Gerät wird abgelehnt.
- Standortzugriff. SIMPro fragt danach, sobald Tap to Pay das erste Mal läuft. Der Grund ist nicht Neugier: Stripe braucht das Land, um die Zahlung dem richtigen Regelwerk zuzuordnen. Ohne die Erlaubnis geht es nicht.
4. In SIMPro
Einstellungen → Kartenzahlung → Stripe Tap to Pay. Der Eintrag ist ausgegraut, bis der Betreiber ihn freischaltet. Danach erscheinen zwei Felder und eine Liste mit drei Häkchen — freigeschaltet, Schlüssel hinterlegt, Standort hinterlegt. Erst wenn alle drei grün sind, kassiert die App damit.
Der Selbstfragebogen zur Kartensicherheit (PCI DSS)
Wer Kartenzahlungen annimmt, hat gegenüber seiner Bank einen Selbstfragebogen auszufüllen (SAQ, Self-Assessment Questionnaire). Welcher, hängt davon ab, wo die Kartendaten durchlaufen.
| Weg | Fragebogen | Warum |
|---|---|---|
| Terminal ohne Kopplung | SAQ B oder B-IP | Die Karte sieht nur das Terminal |
| Viva-App auf dem Telefon | SAQ B-IP | Die Karte sieht nur die App des Anbieters |
| Tap to Pay | SAQ B-IP (nach Stripes Angabe) | Die Karte liest das Stripe-SDK, nicht SIMPro |
SIMPro sieht die Kartennummer nie. Zurück kommen die maskierte Nummer (515876******4490), die Kartenart und die Referenz des Anbieters — mehr gibt es an keiner Stelle, und mehr wird an keiner Stelle gespeichert. Das ist der Grund, warum kein Kartendatenumfeld entsteht, das jährlich zu bescheinigen wäre.
Das ersetzt keine Auskunft Ihrer Bank. Welcher Fragebogen für Ihren Vertrag gilt, sagt Ihr Zahlungsdienstleister. Die Tabelle nennt, was üblicherweise zutrifft, damit Sie die richtige Frage stellen können.
Was Tap to Pay nicht ändert
Nichts an Kapitel 8c: eine Kartenzahlung am Ort der Leistung ist ein Barumsatz nach § 131b BAO, ganz gleich ob am Terminal, über eine Anbieter-App oder am Telefon. Sie zählt in die Grenze von 7.500 € und braucht ab den Umsatzgrenzen dieselbe Signatur.
8g. Cloud-Betrieb
Die eigene Installation bei Hetzner: ein Server, eine Adresse, eine Sicherung, die woanders liegt. Was hier steht, gilt für simpro-cloud-1 unter app.simpro-software.at — und für jede weitere, die mit denselben Skripten entsteht.
Alle Skripte liegen unter
deploy/cloud/und sind idempotent. Jede Stufe fragt zuerst, was schon da ist, und legt nur an, was fehlt. Zweimal aufrufen kostet nichts. Bricht ein Nachtlauf ab, wird er nicht neu begonnen, sondern fortgesetzt.
Der Aufbau
| Stufe | Was sie tut |
|---|---|
01-server-anlegen.sh | SSH-Schlüssel, Firewall, Server bei Hetzner, Snapshots |
02-haerten.sh | Updates, kein Root, fail2ban, Zeitzone, Benutzer, Tailscale |
03-stack.sh | Docker, der Stapel dieses Repositories, .env, Caddy |
04-dns.sh | A-Eintrag bei Cloudflare, DNS only |
05-selbstaktualisierung.sh | systemd-Timer für scripts/update.sh |
06-backup.sh | restic auf die Storage Box, Wiederherstellungstest |
07-ueberwachung.sh | UptimeRobot von außen, Mailweg von innen |
08-ersteinrichtung.sh | Mandant, Verwalter mit zweitem Faktor, Dienst-Token |
09-abnahme.sh | Geht jeden Punkt durch und sagt, woran es hängt |
Es ist derselbe Stapel wie überall. compose.yml, docker/ und scripts/ gehen unverändert aus dem Repository auf den Server; Caddy im Profil standalone ist der einzige Eingang und holt sein Zertifikat selbst. Kein zweiter Cloud-Stapel — der wäre ein zweiter Ort, an dem etwas anderes gilt, und der Unterschied fiele erst auf, wenn er weh tut.
Auf dem Server liegt kein Git-Token. Der Stapel wird von der Arbeitsmaschine hinübergelegt, nicht dort geklont. Das einzige Geheimnis, das auf der Maschine liegen muss, ist das Pull-Token für ghcr — das braucht die Selbstaktualisierung ohnehin.
Was wo liegt
| Der Stapel | /opt/simpro auf dem Server |
| Daten, Sicherungen, Zertifikate | /opt/simpro/data |
| Zugangsdaten (Passwort, TOTP) | B:\Rechnung-privat\simpro-cloud-zugang.txt |
| Der SSH-Schlüssel | B:\Rechnung-privat\simpro-cloud-1.ed25519 |
| Der restic-Schlüssel | B:\Rechnung-privat\simpro-cloud-restic.txt |
Ohne den restic-Schlüssel ist das Backup wertlos. Es gibt keinen zweiten Weg hinein: restic kennt keine Hintertür, und Hetzner kann ihn nicht zurücksetzen. Brennt der Server ab und ist der Schlüssel weg, sind die Sicherungen unbrauchbarer Zufallssalat. Er gehört an einen zweiten Ort, der nicht dieselbe Festplatte ist.
Zugang über Tailscale
Nach der Härtung ist SSH nur noch über Tailscale erreichbar; Port 22 ist in der Firewall geschlossen. Der Weg hinein:
# Einmalig auf dem eigenen Rechner
tailscale up
# Danach
ssh simpro@simpro-cloud-1
Solange tailscale-authkey.txt fehlt, ist Tailscale zwar installiert, tritt aber keinem Netz bei — und Port 22 bleibt dann offen, weil er sonst der einzige Weg auf die Maschine wäre. Ein Server, den niemand mehr erreicht, ist kein sicherer Server, sondern ein verlorener. Sobald der Schlüssel da ist: ./deploy/cloud/02-haerten.sh noch einmal laufen lassen, der Rest geschieht von selbst.
Updates
Ein systemd-Timer fragt stündlich nach:
systemctl list-timers simpro-update.timer
journalctl -u simpro-update.service -n 50
Das Skript tut nichts, solange der Server nichts angefordert hat. Der Server entscheidet, wann aktualisiert wird (UPDATE_WINDOW in der .env), nicht der Timer; stündlich zu fragen heißt nur, dass das Fenster zuverlässig getroffen wird, auch wenn die Maschine zwischendurch aus war.
Der Ablauf ist derselbe wie unter Windows: Sicherung, Abbild ziehen, anhalten, Migrationen, starten, /health — und bei einem Fehlschlag alles zurück. Von Hand:
cd /opt/simpro
./scripts/update.sh --pruefen # nur nachsehen
./scripts/update.sh --fassung 0.3.5 # genau diese einspielen
Wiederherstellung, Schritt für Schritt
Der Ernstfall: die Datenbank ist hin, der Server steht noch.
cd /opt/simpro
# 1. Was liegt da?
docker compose run --rm backup restore.sh --list
# 2. Zurückspielen. Ohne --snapshot nimmt es den jüngsten.
docker compose run --rm backup restore.sh --snapshot latest
# 3. Nachsehen
docker compose exec -T db psql -U rechnung -d rechnung -c 'SELECT count(*) FROM "Document";'
Der schlimmere Ernstfall: der Server ist weg. Dann ist die Reihenfolge:
| 1 | ./deploy/cloud/01-server-anlegen.sh — neue Maschine, gleicher Name |
| 2 | ./deploy/cloud/02-haerten.sh und 03-stack.sh |
| 3 | Den restic-Schlüssel aus simpro-cloud-restic.txt in die neue .env |
| 4 | ./deploy/cloud/06-backup.sh — der Schlüssel für die Storage Box |
| 5 | docker compose run --rm backup restore.sh --snapshot latest |
| 6 | ./deploy/cloud/04-dns.sh — der Name zeigt auf die neue Adresse |
Schritt 3 ist der, an dem es scheitert, wenn der Schlüssel nicht da ist. Alles andere lässt sich neu bauen.
Die Sicherung, und wie man ihr glaubt
Täglich um 02:30, verschlüsselt, auf die Hetzner Storage Box über SFTP. Vorgehalten werden 30 Tage täglich und 12 Monate monatlich.
Am ersten des Monats um 04:00 wird sie zurückgespielt — automatisch, in eine Wegwerf-Datenbank, mit einem Vergleich der Belegzahl. Eine Sicherung, die nie zurückgespielt wurde, ist eine Behauptung: restic check sagt, dass die Blöcke zusammenpassen, nicht dass im Auszug etwas Brauchbares steht.
systemctl list-timers simpro-wiederherstellungstest.timer
./scripts/wiederherstellungstest.sh --ziel remote # von Hand
Schlägt er fehl, entsteht eine BackupRun-Zeile mit dem Zustand FAILED, und die stündliche Überwachung macht daraus eine Mail an den Betreiber.
Was gemeldet wird, und von wem
Von außen schaut UptimeRobot alle fünf Minuten auf https://app.simpro-software.at/health. Das ist der einzige Weg, der auch dann noch funktioniert, wenn der Server tot ist — ein Server, der sein eigenes Sterben melden soll, kommt nicht mehr dazu.
Von innen meldet der Server, was von außen niemand sieht:
| Was | Wann |
|---|---|
| Die Sicherung ist fehlgeschlagen oder ausgeblieben | stündlich geprüft |
| Der Plattenplatz fällt unter 15 % | stündlich geprüft, täglich gemeldet |
| Das Zertifikat läuft in weniger als 14 Tagen ab | stündlich geprüft, täglich gemeldet |
| Es ist gar kein Zertifikat zu sehen | dasselbe |
Alle vier gehen an ADMIN_EMAIL. Ohne eingerichteten Postausgang entstehen sie, aber niemand bekommt sie — dann stehen sie nur im Protokoll. Ein SMTP-Zugang gehört deshalb zur Einrichtung dazu.
Der Ausfallplan
| Was ist los | Was zu tun ist | |
|---|---|---|
| UptimeRobot meldet Ausfall, SSH geht | docker compose ps, dann docker compose logs api --tail 100 | |
| UptimeRobot meldet Ausfall, SSH geht nicht | Hetzner-Konsole: Server neu starten. Hilft das nicht, aus dem letzten Snapshot wiederherstellen | |
| Zertifikat abgelaufen | `docker compose logs caddy \ | tail -50`. Fast immer ist Port 80 zu — die ACME-Prüfung kommt darüber |
| Datenbank kaputt | Wiederherstellung, oben | |
| Server ganz weg | Neu aufsetzen, oben. Der restic-Schlüssel ist die Bedingung | |
| Update hat den Server zerlegt | Es rollt selbst zurück. Tut es das nicht: ./scripts/update.sh --fassung <vorige> |
Die Monatsroutine
Einmal im Monat, zehn Minuten:
| 1 | ./deploy/cloud/09-abnahme.sh — geht alle Punkte durch |
| 2 | Nachsehen, dass der Wiederherstellungstest gelaufen ist und gestimmt hat |
| 3 | Plattenplatz ansehen: df -h /opt/simpro |
| 4 | Hetzner-Rechnung ansehen — ein vergessener Server kostet weiter |
| 5 | Prüfen, dass die Zugangsdateien in Rechnung-privat noch da und lesbar sind |
Punkt 5 ist der, den alle auslassen, und der einzige, dessen Fehlen man erst im Ernstfall bemerkt.
Was der Betrieb kostet
| Server cx33 (4 Kerne, 8 GB, 80 GB) in Nürnberg | 8,49 € / Monat brutto |
| Snapshots (20 % vom Server) | 1,70 € / Monat |
| Storage Box BX11 (1 TB) | ab 3,81 € / Monat brutto |
| Domain, Cloudflare, UptimeRobot (kostenloser Tarif) | 0 € |
| Zusammen | rund 14 € / Monat |
8h. Die Lizenz
Je Betrieb eine Lizenz mit Schlüssel, Laufzeit, Betriebsmodell und Grenzen. Was sie bedeutet, entscheidet eine Sache: ob Neues angelegt werden darf. Alles andere geht immer.
Es gibt keinen Zustand „gesperrt". Eine abgelaufene Lizenz führt in den Lesemodus: ansehen, exportieren, drucken, Datenträger überlassen — alles das geht weiter, dauerhaft. Auch Sicherheitsupdates laufen weiter.
Das ist keine Großzügigkeit, sondern § 132 BAO: die Aufzeichnungen sind sieben Jahre lesbar zu halten. Wer sie hinter eine Bezahlschranke stellt, macht sich zum Mitverursacher eines Verstoßes, den ein anderer begeht.
Die Zustände
| Was dasteht | Was gilt |
|---|---|
| Die Lizenz gilt bis … | Alles normal |
| Der Lizenzserver antwortet nicht | Es wird weitergearbeitet. Ab dem zweiten Tag steht daneben, wie viele Tage die Kulanz noch läuft |
| Die Lizenz ist abgelaufen | Lesemodus |
| Die Lizenz wurde zurückgezogen | Lesemodus, mit dem Grund |
| Seit 30 Tagen nicht bestätigt | Lesemodus. Sobald die Verbindung wieder steht, geht es ohne Zutun weiter |
| Keine Lizenz hinterlegt | Alles normal — eine frische Installation soll nicht an einem Lizenzserver hängen, den sie noch nicht kennt |
Dreißig Tage Kulanz, gerechnet ab der letzten geglückten Antwort. Der Fall ist nicht selten und fast nie böse: ein Netzausfall, eine Firewall, ein abgelaufenes Zertifikat auf meiner Seite. Wer dann am dritten Tag stillsteht, hat ein Problem, das ich verursacht habe.
Was in der .env steht
# Der eigene Schluessel. Er steht auf der Rechnung.
LIZENZ_SCHLUESSEL=SIMPRO-ABCD-EFGH-JKLM-NPQR
# Wen dieser Server fragt. Ohne Vorbelegung: eine Installation, die von sich
# aus bei einem fremden Server anklopft, ist genau das, was niemand will.
LIZENZ_SERVER=https://app.simpro-software.at
Gefragt wird einmal am Tag, um 02:05, zusammen mit der Updateprüfung. Wer gerade verlängert hat, drückt in Einstellungen → Lizenz auf Jetzt nachfragen, statt bis zum nächsten Morgen zu warten.
Für mich: der Lizenzserver
Nur auf meinem Server, und nur dort gibt es die Verwaltung:
LIZENZSERVER=true
# Der Freigabeschalter fuer Eigenserver. Aus, mit Absicht.
LIZENZ_EIGENSERVER_ERLAUBT=false
Und in
compose.ymldurchreichen. Bis 0.4.5 standLIZENZSERVERin der.envund kam nie im Container an; der Server hielt sich für eine gewöhnliche Kundeninstallation und antwortete „Diese Installation stellt keine Lizenzen aus". Eine Einstellung, die nicht imenvironment:-Block des Dienstesapisteht, gibt es für das Programm nicht. Prüfen lässt sich das in einem Satz:docker compose exec api printenv | grep LIZENZ
Ausstellen kann nur, wer beides ist: Systemverwalter und auf dieser Installation angemeldet. Die zwei Bedingungen sind mit Absicht getrennt — die eine sagt, welcher Server die Rolle spielt, die andere, welcher Mensch sie ausüben darf. An ein Konto allein lässt sie sich nicht binden: ein Konto reist mit, ein Lizenzserver nicht. Auf einer Installation ohne LIZENZSERVER erscheint der Knopf Ausstellen deshalb gar nicht, sondern ein Satz, der sagt, warum.
Mein eigener Betrieb braucht keine Lizenz. Erkannt wird er daran, dass ein Systemverwalter in ihm Mitglied ist — dieselbe Formulierung wie beim Schutz vor dem Löschen, und aus demselben Grund: sie hält sich selbst aktuell. Ohne diese Ausnahme entstünde ein Kreis ohne Ausgang, denn im Lesemodus ließe sich auch keine Lizenz mehr anlegen. In der App steht dort „Betreiber (lizenzfrei)".
Schlüssel auf Vorrat
Im Ausstellen-Dialog steht als erste Wahl „Auf Vorrat — noch kein Betrieb". Ein solcher Schlüssel entsteht ohne Betrieb und bindet sich an den ersten, der ihn auf seiner Lizenzseite einlöst — genau einmal, danach an keinen zweiten. Das ist der Weg für einen Interessenten, den es hier als Betrieb noch gar nicht gibt: er registriert sich, legt seinen Betrieb an, trägt den Schlüssel ein.
In der Liste steht so eine Zeile als „Auf Vorrat — noch nicht eingelöst", bis sie jemand einlöst.
Wie lange eine Lizenz laufen darf
| Art | Laufzeit |
|---|---|
| Test | höchstens 365 Tage |
| Abo | frei, auch unbefristet |
Der Deckel für Testlizenzen sitzt im Server, nicht im Formular — eine Grenze, die nur ein Eingabefeld kennt, ist keine. Die Absicht ist kaufmännisch: eine Abolizenz entsteht künftig mit dem Abo und wird dort verlängert; was ich von Hand ausstelle, ist ausdrücklich ein vorübergehendes Recht und soll nicht versehentlich zur unbefristeten Freigabe werden.
Seit 0.4.2 gibt es dafür einen Bildschirm: Verwaltung → Lizenzen. Liste mit dem Zustand je Zeile (aktiv, Kulanz, abgelaufen, widerrufen), Ausstellen mit Betrieb, Laufzeit, Betriebsmodell und Grenzen, Verlängern und Widerrufen je Zeile mit Rückfrage, Verlauf je Lizenz. Der Schlüssel steht einmal nach dem Ausstellen da — er gehört in die .env der Installation, für die er gilt.
Die Auswahl Betriebsmodell erscheint nur, wenn LIZENZ_EIGENSERVER_ERLAUBT gesetzt ist. Steht der Schalter aus, weist der Server jede Eigenserver-Lizenz ohnehin ab, und ein Feld, dessen Benutzung immer mit einer Absage endet, wäre eine Falle.
Darunter liegen weiterhin dieselben Endpunkte:
| Was | Wie |
|---|---|
| Alle Lizenzen | GET /api/v1/lizenz/verwaltung → { lizenzen, eigenserverErlaubt, istLizenzserver, testlizenzMaxTage } |
| Betriebe zum Ausstellen | GET /api/v1/lizenz/verwaltung/mandanten — mit hatLizenz je Betrieb |
| Ausstellen | POST /api/v1/lizenz/verwaltung mit betriebsmodus, laufzeitTage, typ — tenantId weglassen ergibt einen Schlüssel auf Vorrat |
| Einlösen (der Kunde) | POST /api/v1/lizenz/einloesen mit schluessel — nur ADMIN des Betriebs |
| Verlängern | POST …/:id/verlaengern mit tage |
| Widerrufen | POST …/:id/widerrufen mit grund — der Grund ist Pflicht |
| Zurücknehmen | POST …/:id/wiederherstellen |
| Historie | GET …/:id/historie |
Alle davon verlangen die Systemverwalter-Fahne — siehe den nächsten Abschnitt. ADMIN allein reicht nicht mehr, und das ist der Punkt: auf dem Lizenzserver hätte der Verwalter eines Kunden sonst die Lizenz eines anderen in der Hand gehabt.
Die Verwaltung arbeitet auf der Eigentümerverbindung: sie schreibt Zeilen fremder Mandanten und liest über alle hinweg. Die Anwendungsrolle darf Lizenzen nur lesen — eine Installation, die ihre eigene Lizenz verlängern kann, braucht keine.
Verlängern rechnet ab dem alten Ende, oder ab heute, wenn die Lizenz schon abgelaufen war. Sonst verschenkte eine Verlängerung nach drei Monaten Pause die drei Monate, und der Kunde zahlte für eine Zeit, die vorbei ist.
Systemverwalter: wer die Installation verwaltet
Bis 0.4.1 gab es genau eine Sorte Verwalter: ADMIN, und zwar je Mandant. Wer seinen eigenen Betrieb verwaltete, durfte damit auch alles, was die Installation angeht — Mandanten anlegen, Dienst-Token lesen, Updates auslösen, Lizenzen vergeben und zurückziehen. Auf einer Installation mit einer Firma fällt das nicht auf, weil beide dieselbe Person sind. Auf dem Lizenzserver war es ein Loch.
Seit 0.4.2 trennt das die Fahne istSystemverwalter. Sie wird nicht über die Oberfläche vergeben, sondern an der Maschine:
# Wer sie hat
docker compose exec api node dist/cli.js systemverwalter
# Vergeben und wieder nehmen
docker compose exec api node dist/cli.js systemverwalter --email ich@example.at --an
docker compose exec api node dist/cli.js systemverwalter --email ich@example.at --aus
Der Befehl weigert sich, den letzten Träger zu entfernen — sonst wäre die Installation nicht mehr zu verwalten, und der Weg zurück führte über die Datenbank.
Zwei Dinge, die leicht zu verwechseln sind:
- Die Fahne gilt installationsweit, nicht je Mandant. Sie macht niemanden zum Administrator eines Betriebs, in dem er nichts verloren hat —
@Rolesgilt weiter daneben. - Der Wächter liest sie bei jedem Aufruf aus der Datenbank und nicht aus dem Zugangstoken. Wer sie entzieht, entzieht sie sofort; sonst behielte ein zurückgezogener Systemverwalter seine Rechte bis zum Ablauf des Tokens, also eine Viertelstunde lang — genau die Viertelstunde, in der man es nicht will.
Die Migration setzt sie beim Umstieg auf 0.4.2 für jeden bestehenden Administrator: sie führt eine Unterscheidung ein und nimmt niemandem etwas weg. Wer mehrere Kunden auf einer Installation hat, nimmt sie danach von Hand wieder weg — der Befehl oben mit --aus.
Einen neuen Kunden anlegen — in einem Zug
Verwaltung → Mandanten → Firma anlegen. Ein neuer Kunde braucht drei Dinge, und wer sie einzeln zusammensucht, vergisst das dritte:
- Die Firma — Name, Kurzzeichen, Farbe, wahlweise Grunddaten aus einer bestehenden übernommen.
- Den ersten Administrator — E-Mail und Name. Er bekommt ein Erstpasswort, das genau einmal angezeigt wird und beim ersten Anmelden gewechselt werden muss.
- Eine Testlizenz — gratis, mit frei gewählter Laufzeit.
Beides Letztere ist abwählbar, steht aber vorgeschlagen da, statt still zu fehlen. Schlägt der zweite oder dritte Schritt fehl, bleibt die Firma bestehen und der Fehler wird gemeldet — ein halb angelegter Kunde ist ärgerlich, ein verschwundener wäre schlimmer.
Lizenzarten: Test und Abo
Seit 0.4.5 trägt jede Lizenz eine Art, und der Unterschied ist nicht technisch — beide laufen beim Ablauf in denselben Lesemodus. Er ist kaufmännisch, und er gehört trotzdem in die Datenbank: sonst steht in einem halben Jahr eine Liste von vierzig Lizenzen da, und niemand weiß mehr, welche davon je bezahlt wurde.
| Art | Wofür |
|---|---|
| TEST | gratis, mit Laufzeit. Läuft aus und wird nicht von selbst wieder gültig |
| ABO | zahlender Betrieb. Verlängert sich später über die Zahlung; bis Stripe kommt, von Hand |
Alles, was vor 0.4.5 ausgestellt wurde, steht als TEST — es war zum Ausprobieren da, und die Annahme ist die vorsichtigere: eine Testlizenz fälschlich als Abo zu führen wäre eine Rechnung, die niemand erwartet.
Ein Betrieb mit Lizenz wird nicht mehr ausgegraut. Er steht in der Auswahl mit dem Vermerk hat bereits eine, und der Knopf heißt dann Ersetzen: die bisherige endet sofort, der alte Schlüssel gilt nicht mehr, der Verlauf hält fest, was vorher galt. Wer nur die Laufzeit verlängern will, benutzt in der Liste Verlängern — das lässt den Schlüssel stehen.
Warum ersetzen und nicht danebenstellen: ein Betrieb hat genau eine Lizenz; die Spalte ist eindeutig. Zwei gleichzeitig gültige für denselben Betrieb wären eine Frage ohne Antwort.
Wenn ein Kunde sich selbst löscht
Ein Betrieb kann seit 0.4.5 den Weg hinaus selbst gehen — Einstellungen → Betrieb → Betrieb löschen, nur für seine Administratoren. Vier Schritte (Export, Warnhinweis, erneute Anmeldung, Firmenname), dann sieben Tage Frist.
Für Sie als Betreiber heißt das:
- In Verwaltung → Mandanten steht bei dem Betrieb ein roter Streifen mit dem Vollzugsdatum und dem Namen dessen, der es angefordert hat.
- Sie können abbrechen — der Knopf daneben. Der Betrieb selbst kann es ebenso; Ihr Weg ist der zweite, nicht der einzige. Für den Anruf „das war ein Versehen".
- Nach Ablauf räumt der nächtliche Lauf um 03:45 ab — nach der Sicherung um 02:30, damit der letzte Stand noch gesichert ist.
- Danach steht im Prüfprotokoll der Installation ein Eintrag mit Name, Anforderer, Zeitpunkt der Anforderung und des Vollzugs und der Bestätigung, dass Export und Warnhinweis erledigt waren. Das ist Ihr Nachweis, wenn in einem Jahr jemand fragt.
Was Sie nicht können: einen fremden Betrieb mit festgeschriebenen Belegen hart löschen. Das bleibt der Weg des Mandanten selbst; Ihnen steht das Stilllegen offen.
Konten von der Maschine aus
Zwei Handgriffe, die es nur in der App gab — und die man genau dann braucht, wenn man nicht in die App kommt.
# Ein Konto wieder aktivieren (oder mit --aus stilllegen)
docker compose exec api node dist/cli.js benutzer-aktiv --email office@firma.at
# Ein Erstpasswort setzen, immer mit Änderungszwang.
# Ohne --passwort würfelt der Server eines und zeigt es an.
docker compose exec api node dist/cli.js reset-password --email office@firma.at --passwort 'mindestens-zwoelf-zeichen'
Warum das Aktivieren auf die Kommandozeile gehört: Deaktivieren ging immer über die Verwaltung. Der Weg zurück führte bisher auch nur dorthin — und genau dort steht man, wenn er gebraucht wird. Wird der letzte Administrator eines Betriebs versehentlich stillgelegt, kommt niemand mehr an die Verwaltung, um ihn zu reaktivieren. Bliebe SQL, und Handarbeit an der Datenbank ist das, was hier niemand tun soll.
Beide Befehle schreiben ins Prüfprotokoll — dass es geschah und für wen. Das Passwort steht dort nicht; ein Protokoll, das Passwörter mitschreibt, ist ein Schlüsselbund, und es ist unlöschbar.
Einen Mandanten löschen oder stilllegen
Verwaltung → Mandanten, den Betrieb aufklappen. Was dort angeboten wird, rechnet der Server — und er rechnet es beim Eingriff noch einmal nach.
Löschen gibt es nur für einen Betrieb, der nie ernsthaft gearbeitet hat: kein festgeschriebener Beleg, keine Ausgabe. Das ist der Testmandant, der Vorführmandant, der versehentlich zweimal angelegte. Der Dialog verlangt den Firmennamen eingetippt — ein Häkchen setzt man aus Versehen, einen Firmennamen tippt niemand versehentlich ab.
Stilllegen ist der Weg für alle anderen. Sobald ein Beleg festgeschrieben ist, trägt er eine Nummer aus einem lückenlosen Kreis und fällt unter § 132 BAO — sieben Jahre lesbar. Ihn zu löschen, weil ein Vertrag endet, wäre die Mitwirkung an einem Verstoß, den ein anderer begeht. Stillgelegt heißt: keine Anmeldung mehr, alle Sitzungen enden, die Lizenz endet — die Aufzeichnungen bleiben unverändert. Rückgängig über Wieder zulassen; die Lizenz kommt dabei nicht von selbst zurück, das ist eine kaufmännische Entscheidung.
Was in der Mandantenliste steht
Seit 0.4.8 stehen dort alle Betriebe der Installation, nicht nur die, in denen ich selbst Administrator bin. Der Anlass war ein Loch, das erst im Betrieb auffiel: nachdem mein Betreiberkonto in einen eigenen Mandanten umgezogen war, verschwanden die Kunden aus der Verwaltung — und mit ihnen der einzige Weg zur Betreuungssicht, denn die braucht die Kennung des Betriebs.
Ein fremder Betrieb ist als solcher gekennzeichnet („· Betreuung" in der Zeile). Aufgeklappt bietet er genau zwei Dinge:
- Betreuung — die fünf Handgriffe an Konten: anlegen, Passwort zurücksetzen, entsperren, zweiten Faktor zurücksetzen, stilllegen oder wieder zulassen. Jeder davon steht im Prüfprotokoll des Kunden.
- Lebenszyklus — stilllegen, wieder zulassen, löschen (unter den Bedingungen unten), eine angekündigte Selbstlöschung abbrechen.
Was dort nicht steht, ist der Punkt: keine Belege, keine Kunden, keine Umsätze. Nicht ausgeblendet — es gibt keinen Weg dorthin, und die Zeilensicherheit gäbe ihn auch nicht her. Ein gewöhnlicher Mandanten-Administrator sieht die fremden Betriebe gar nicht erst; für ihn ist die Liste unverändert.
Zwei Betriebe sind unantastbar, und der Weg wird dort gar nicht erst angeboten: der, in dem Sie gerade angemeldet sind, und jeder, in dem ein Systemverwalter arbeitet. Die zweite Regel ist absichtlich so formuliert statt über eine gesetzte Kennzeichnung — sie hält sich selbst aktuell.
Was ein Löschen hinterlässt: die Einträge des Prüfprotokolls und eine geleerte Hülle namens „Gelöschter Mandant". Beides, weil die Protokollkette darauf zeigt und ein Loch darin den Integritätsnachweis der ganzen Installation kostet. Die Begründung steht in docs/DECISIONS.md, N38. Der Vollzug selbst bekommt einen Eintrag auf Installationsebene: Name, wer, wann, wie viele Zeilen — der Nachweis, den Sie brauchen, wenn in einem Jahr jemand fragt, wo die Daten geblieben sind.
8h1. Was zum Ausrollen dazugehört — und die Drift-Warnung
Das Abbild ist nicht die ganze Installation
update.sh tauscht das Abbild und sonst nichts. Alles, was den Stapel zusammensteckt, liegt daneben auf der Platte und bleibt liegen:
| Datei | Was darin steht |
|---|---|
compose.yml | welche Dienste laufen, welche Umgebungsvariable an welchen geht, welcher Ordner wohin gehängt wird |
docker/Caddyfile.standalone | die Namen, die Caddy bedient, wenn dieser Stapel die öffentlichen Ports hat |
docker/Caddyfile.behind-proxy | dasselbe, wenn ein fremder Proxy davor steht |
docker/Caddyfile.lan | dasselbe im eigenen Netz, ohne öffentliches Zertifikat |
scripts/update.sh | dieses Skript selbst |
Die Liste steht an einer Stelle: scripts/ausrollen.txt. Wer eine Datei hinzunimmt, trägt sie dort ein — und beide Skripte kennen sie ab dann.
Nicht dabei: die .env. Sie gehört dem Server, enthält Geheimnisse und unterscheidet sich absichtlich von Haus zu Haus. Ebenso wenig die Dockerfile.* und entrypoint-api.sh: die stecken im Abbild, und was davon am Server liegt, tut nichts.
Wer sie hinüberlegt
# Website und Konfiguration in einem Zug (der uebliche Weg beim Betreiber)
./deploy/cloud/12-website.sh
Es legt compose.yml und die Caddyfiles mit hinüber, sichert vorher die bisherigen als *.vor-website und startet Caddy mit --force-recreate. Das --force-recreate ist kein Beiwerk: der Caddyfile hängt als Bind-Mount im Container, und eine geänderte Datei ändert die Dienstbeschreibung nicht — compose sähe sonst keinen Grund, etwas anzufassen, und meldete „Running".
Auf einer Installation ohne Website geht es von Hand, mit demselben Ergebnis:
scp compose.yml docker/Caddyfile.standalone benutzer@server:/opt/simpro/…
ssh … 'cd /opt/simpro && docker compose up -d --force-recreate caddy'
Die Drift-Warnung
Seit 0.5.2 bringt jedes Abbild die Prüfsummen der Konfiguration mit, mit der es gebaut wurde (/app/ausgerollt.sha256). update.sh liest sie vor dem Abbildtausch aus dem frisch gezogenen Abbild und hält sie gegen das, was am Server liegt.
Läuft alles zusammen:
07:19:03 Konfiguration am Server ist der Stand von 0.5.2.
Weicht etwas ab:
07:19:03 ! compose.yml weicht vom Stand der Fassung 0.5.2 ab
07:19:03 ACHTUNG: 1 Konfigurationsdatei(en) am Server sind nicht der Stand von 0.5.2.
07:19:03 Das Update laeuft trotzdem - ueberschrieben wird hier nichts, es koennte
07:19:03 eine bewusste Aenderung sein. Aber: was in einer neuen compose.yml oder
07:19:03 einem neuen Caddyfile steht, wirkt erst, wenn die Datei am Server liegt.
Das Update läuft weiter. Die Prüfung warnt und überschreibt nicht — am Server kann eine bewusste Änderung liegen, eine zusätzliche Kopfzeile, ein anderer Port, eine Zeile für eine Besonderheit des Hauses. Ein Skript, das die stumm überschreibt, ist schlimmer als der Drift selbst: der Drift kostet eine Stunde Suchen, das Überschreiben kostet die Änderung.
Was zu tun ist, wenn die Warnung kommt:
- Nachsehen, was anders ist.
diffgegen die Datei aus dem Repository der ausgerollten Fassung. - Ist es eine Änderung, die Sie gemacht haben und behalten wollen: die neue Fassung der Datei holen und Ihre Zeile dort wieder eintragen.
- Ist es keine: die Datei hinüberlegen und Caddy bzw. den betroffenen Dienst mit
--force-recreateneu starten.
Zwei Zeilen, die keine Warnung sind:
· docker/Caddyfile.behind-proxy liegt hier nicht (gehoert zu einer anderen Betriebsart?)
Die drei Caddyfiles gehören zu drei Betriebsarten, und jede Installation fährt genau eine davon.
Das Abbild bringt keine Konfigurationspruefsummen mit - Drift-Pruefung uebersprungen.
Ein Abbild vor 0.5.2. Die Prüfung greift ab dem ersten Update auf 0.5.2 oder neuer.
8i. Die öffentliche Website
Seit 0.5.0 gibt es unter www.simpro-software.at ein Schaufenster mit Selbstregistrierung. Es liegt auf demselben Server wie die Anwendung, aber unter einem eigenen Namen und mit eigenem Zertifikat.
| Name | Was dort liegt |
|---|---|
app.simpro-software.at | die Anwendung. Gehört den Kunden |
www.simpro-software.at | die Website |
simpro-software.at | leitet auf www weiter |
Bauen und ausliefern
node website/bauen.js # erzeugt website/dist
./deploy/cloud/12-website.sh # baut, legt hinueber, schaltet Caddy frei
Das Skript legt den Ordner erst vollständig daneben und tauscht dann — ein halb entpacktes Verzeichnis wäre eine halbe Website unter dem Namen, den ein Kunde gerade aufruft. Danach lädt es nur Caddy neu; die API blinzelt dabei nicht.
Die Namen stehen in der .env:
WEBSITE_DOMAIN=www.simpro-software.at
WEBSITE_DOMAIN_NACKT=simpro-software.at
WEBSITE_DIR=./website
WEBSITE_URL=https://www.simpro-software.at
Leer lassen heißt: es gibt keine Website. Eine Installation beim Kunden hat kein Schaufenster und soll auch kein Zertifikat dafür beantragen. Der Caddy-Block ist dann aus.
WEBSITE_URL ist die Adresse, die in den Bestätigungs-E-Mails steht. Sie falsch zu setzen heißt: Links, die nirgendwo hinführen.
Was die Website vom Server holt
Nur drei Zweige gehen durch, alles andere ist eine Datei:
| Weg | Wofür |
|---|---|
/api/v1/oeffentlich/* | Registrierung, Bestätigung, Abo-Platzhalter |
/api/v1/auth/email/bestaetigen | der Link für eine bestehende Adresse |
/downloads/aktuell | Fassungsnummer und Dateien des letzten Release |
/downloads/aktuell fragt einmal je Stunde bei GitHub nach und merkt sich die Antwort. Nicht der Browser des Besuchers — das wäre eine Verbindung zu einem Dritten, und die Datenschutzerklärung sagt, dass es hier keine gibt.
Die Selbstregistrierung
# An oder aus. Vorgabe: an.
REGISTRIERUNG_OFFEN=true
# Wie viele Betriebe hoechstens je Tag entstehen duerfen. Vorgabe: 20.
REGISTRIERUNG_JE_TAG=20
Der Ablauf ist zwingend dieser:
- Das Formular legt nur eine Bestätigungszeile an.
- Eine E-Mail geht hinaus, der Link gilt 48 Stunden.
- Erst der Klick erzeugt Mandant, Administrator, Grunddaten und eine TEST-Lizenz über 30 Tage.
Ohne Klick verfällt die Anmeldung rückstandslos. Warum das so herum ist, steht in docs/DECISIONS.md, N46.
Gegen Automaten stehen vier Bremsen: ein Honigtopffeld, die Drossel des Endpunkts (10 je Stunde), eine Grenze je IP (5 je Stunde) und eine je E-Mail-Domäne (10 je Tag). Kein Captcha — das wäre eine Einbindung bei einem Dritten.
Selbstregistrierte Betriebe erkennen
In Verwaltung → Mandanten steht neben der Kennung „selbstregistriert am …". Sie sind nicht schlechter als andere; Sie sollen nur sehen, welche über Nacht dazugekommen sind.
Das Impressum ist noch nicht fertig
Die Seiten impressum.html, datenschutz.html und bedingungen.html tragen Platzhalter, die mit [BITTE ERGÄNZEN] gekennzeichnet sind. Eine unvollständige Offenlegung ist verwaltungsstrafbewehrt — vor dem öffentlichen Betrieb gehören sie ausgefüllt. Die Liste steht in der Abschlussmeldung zu 0.5.0 und in docs/OFFEN-website.md.
Einen Betrieb an der Maschine anlegen
Der Weg über die App ist der übliche: Verwaltung → Mandanten → Neu. Er verlangt aber eine Anmeldung samt zweitem Faktor, und den hat man an der Maschine nicht immer zur Hand. Seit 0.4.7 geht es deshalb auch dort:
# Erst zeigen, was geschaehe - ohne --ja passiert nichts
docker compose exec api node dist/cli.js mandant-anlegen \
--name "Muster GmbH" --admin chef@muster.at
# Dann wirklich; das Erstpasswort steht danach auf stdout
docker compose exec api node dist/cli.js mandant-anlegen \
--name "Muster GmbH" --admin chef@muster.at --ja
Der Befehl legt an, was ein Betrieb zum Arbeiten braucht: den Mandanten, den Hauptstandort, die Nummernkreise und die übrigen Grunddaten — genau wie die App. Ein Betrieb ohne Nummernkreise fällt erst bei der ersten Rechnung auf, und dann ist es zu spät; das ist der Grund, warum dieser Handgriff ein Befehl ist und keine Handarbeit in der Datenbank.
| Schalter | Wofür |
|---|---|
--name | Anzeigename, Pflicht |
--kennung | die Kennung; ohne Angabe aus dem Namen gebildet |
--farbe | Akzentfarbe, etwa #2E7D32 |
--admin | wer den Betrieb verwaltet; ein bestehendes Konto wird genommen, ein unbekanntes angelegt |
--passwort | Erstpasswort für ein neues Konto; ohne Angabe wird eines erzeugt |
--nur-hier | entzieht dem Konto alle anderen Zugehörigkeiten und beendet seine Sitzungen |
--ja | wirklich tun |
--nur-hier ist der Umzug: ein Konto, das künftig nur noch in diesem einen Betrieb arbeiten soll. Die Systemverwalter-Fahne bleibt davon unberührt — sie hängt am Konto, nicht an einer Zugehörigkeit. Das Erstpasswort steht auf stdout und die Erklärung auf stderr, damit sich der Befehl in eine Datei umleiten lässt, ohne den Text mitzunehmen.
Lizenzschlüssel an der Maschine ausstellen
Derselbe Gedanke, dieselbe Form:
# Fuenf Testschluessel auf Vorrat, ein Jahr, ohne Grenzen
docker compose exec -T api node dist/cli.js \
lizenz-ausstellen --anzahl 5 --typ TEST --tage 365 > schluessel.txt
# Oder gleich an einen Betrieb gebunden
docker compose exec api node dist/cli.js \
lizenz-ausstellen --mandant muster-gmbh --typ ABO --tage 0
--tage 0 heißt unbefristet, ohne --mandant entsteht ein Schlüssel auf Vorrat. Die Grenze von 365 Tagen für Testlizenzen gilt hier genauso wie im Server — ein zweiter Weg an derselben Regel vorbei wäre schlimmer als keine Grenze, weil niemand ihn sucht. Jedes Ausstellen bekommt einen Eintrag im Prüfprotokoll mit dem Vermerk ueber: Kommandozeile; es geschieht ja ohne Anmeldung, also ist der Eintrag die einzige Spur.
Einen Mandanten leerräumen
Vor dem echten Betrieb steht in einem Mandanten der Schutt des Ausprobierens: Testrechnungen, erfundene Kunden, Kassenbelege aus dem Sandkasten. Denselben Handgriff braucht später jeder Testkunde-Rückbau.
# Erst zählen - ohne --ja wird nichts gelöscht
docker compose exec api node dist/cli.js mandant-bereinigen --slug simpro-software --durch admin@simpro-software.at
# Dann wirklich
docker compose exec api node dist/cli.js mandant-bereinigen --slug simpro-software --durch admin@simpro-software.at --ja --bestaetigen simpro-software
Was bleibt: der Mandant selbst, die Firmenstammdaten, Nummernkreise, Steuersätze, Textbausteine, E-Mail-Vorlagen, Mahnstufen, der Kontenrahmen, die Standorte, die Benutzer, die Lizenz — und das Prüfprotokoll, das über die Bereinigung einen eigenen Eintrag bekommt.
Was geht: Belege samt Positionen, Zahlungen, Bankimporte, Kunden, Artikel und Bestand, Ausgaben und Anlagen, Buchungen, Kassenbelege, Shop-Anbindungen, Benachrichtigungen, Dateien.
Drei Sicherungen sind eingebaut, und die erste ist die wichtigste:
- Vollständigkeit. Jede der 63 mandantengebundenen Tabellen steht ausdrücklich auf einer der beiden Listen. Kommt später ein Modell dazu, ohne eingeordnet zu werden, bricht der Befehl ab, bevor er etwas anfasst — statt es still mitzunehmen oder still stehenzulassen.
- Wer.
--durchverlangt einen Systemverwalter und steht danach im Prüfprotokoll. - Welchen.
--bestaetigenverlangt die Kennung ein zweites Mal. Ein Tippfehler trifft sonst den falschen Betrieb.
Der Befehl ist wiederholbar: bricht er mitten drin ab, macht ein zweiter Lauf dort weiter, wo der erste stehen blieb. Vorher eine Sicherung, wie vor jedem Eingriff, der nicht rücknehmbar ist.
Eigenserver
Ein Kunde, der selbst hostet, bekommt eine Lizenz mit betriebsmodus: EIGENSERVER. Das geht nur mit gesetztem LIZENZ_EIGENSERVER_ERLAUBT, und dieser Schalter gehört bewusst umgelegt — die Begründung steht in docs/DECISIONS.md, N36.
Je Installation ein eigenes Pull-Token. Beim Ausstellen wird ein ghcr-Token hinterlegt, das nur zu dieser Lizenz gehört:
curl -X POST https://app.simpro-software.at/api/v1/lizenz/verwaltung/<id>/pull-token \
-H 'Authorization: Bearer …' -H 'Content-Type: application/json' \
-d '{"token":"ghp_…"}'
Die Installationsskripte holen es sich über den Lizenzschlüssel:
curl -X POST https://app.simpro-software.at/api/v1/lizenz/pull-token \
-H 'Content-Type: application/json' \
-d '{"schluessel":"SIMPRO-…"}'
Herausgegeben wird es nur, solange die Lizenz gilt. Damit endet mit dem Widerruf der Zugriff auf neue Abbilder; das schon gezogene bleibt, denn niemand soll über Nacht ohne Server dastehen. Mein persönliches Token wandert dabei nie auf einen fremden Server.
Die Cloud-Sicherung ist Bedingung der Freigabe. Der Kunde bekommt einen Unterzugang auf meiner Storage Box:
./deploy/cloud/lizenz-sicherungskonto.sh "Beispiel GmbH"
Der restic-Schlüssel bleibt beim Kunden. Ich sehe Blöcke, keine Rechnungen — und ohne seinen Schlüssel kann ich seine Sicherung nicht lesen. Das ist so gewollt und gehört in das Gespräch, in dem die Freigabe vereinbart wird: die Sicherung liegt bei mir, lesen kann sie nur er. Verliert er den Schlüssel, ist sie weg, und daran kann ich nichts ändern.
Wenn ein Kunde im Lesemodus anruft
Die häufigste Frage ist nicht „warum", sondern „sind meine Daten weg". Die Antwort steht schon auf seinem Bildschirm, aber sie wird beim ersten Schreck nicht gelesen:
| Ansehen, suchen, filtern | geht |
| Drucken und PDF | geht |
| Export, Steuerberater-Datei, Datenträgerüberlassung | geht |
| Sicherheitsupdates | laufen weiter |
| Neue Belege, Kunden, Artikel | gesperrt |
Nach dem Verlängern: Einstellungen → Lizenz → Jetzt nachfragen. Es geht sofort weiter, ohne Neustart.
9. Apps verteilen
Release erzeugen
Ein Release entsteht, sobald eine Marke der Form v0.2.0 gepusht wird. Der Workflow baut den Windows-Installer, das portable ZIP und die Android-APK, erzeugt die Änderungsliste und hängt alles mit Prüfsummen an das GitHub-Release. Ist Play aktiviert, wandert das App Bundle zusätzlich in den internen Testkanal.
git tag v0.2.0
git push origin v0.2.0
Welcher Auftrag wo läuft und was er kostet, steht in docs/RELEASE.md.
Windows
- Setup-Datei vom Release herunterladen und ausführen. Ohne Code-Signatur zeigt Windows eine SmartScreen-Warnung: Weitere Informationen → Trotzdem ausführen.
- Ist Smart App Control aktiv (Windows 11 im Auslieferungszustand), lässt sich die App gar nicht starten. Entweder Smart App Control ausschalten (Windows-Sicherheit → App- & Browsersteuerung; unumkehrbar) oder die Code-Signatur einrichten: Azure Trusted Signing, rund 10 € im Monat, als Secret im Workflow hinterlegt — Anleitung in
docs/RELEASE.md.
Android
- Play Store, interner Test (empfohlen): In der Play Console unter Testen → Interner Test die E-Mail-Adressen der Nutzer eintragen und ihnen den Einladungslink schicken. Voraussetzung: Play-Konto verifiziert, Dienstkonto angelegt,
PLAY_SERVICE_ACCOUNT_JSONals Secret undANDROID_RELEASE_ENABLED=trueals Variable in GitHub. - APK direkt: APK vom Release am Telefon herunterladen, antippen, Installation aus dieser Quelle erlauben. Ab 2027 verlangt Android auch dafür einen verifizierten Entwickler, was mit dem Play-Konto abgedeckt ist.
Der Upload-Schlüssel
Der Ordner mit dem Android-Keystore (
upload-keystore.p12samt Passwort) darf nie verloren gehen. Ohne ihn kann keine neue Fassung auf bestehende Installationen ausgeliefert werden — weder über Play noch als APK. Er gehört an mindestens zwei Orte (Passwortmanager als Anhang, verschlüsselter USB-Stick) und bei der Übergabe an den Betreiber.
10. Organisatorisches und Rechtliches
- Auftragsverarbeitungsvertrag (AVV). Solange der Server nicht beim Betreiber selbst steht, verarbeiten Sie dessen Kundendaten in seinem Auftrag. Vorlage kostenlos bei der WKO; einmal unterschreiben, abheften.
- Konten auf den Betreiber. Domain, Sicherungsspeicher, SMTP — alles, was Geld kostet oder Daten hält, läuft auf seinen Namen. Sie haben Zugriff als Administrator, nicht als Inhaber.
- Zweiter Administrator. Ohne einen zweiten kann niemand den zweiten Faktor des ersten zurücksetzen — und das Programm lässt den letzten aktiven Administrator sich auch nicht selbst herabstufen oder deaktivieren. Angelegt wird er unter Einstellungen → Betrieb → Benutzer und Rollen:

- Zugangsdaten-Übergabe. Der Betreiber braucht in seinem Passwortmanager: Server-Login (SSH oder Windows), GitHub-Zugang, restic-Passwort, Sicherungsspeicher, Keystore-Passwort, Play-Console-Zugang. Falls Ihnen etwas zustößt, muss er das System weiterbetreiben können.
- Aufbewahrung. Belege müssen sieben Jahre aufbewahrt werden (§ 132 BAO). Das System löscht finalisierte Belege nie; die Sicherungen decken die Frist ab, solange der Speicher bezahlt wird.
- Registrierkasse. Ab 0.3.0 kann im System selbst kassiert werden (Kapitel 8c). Vor dem ersten Barumsatz muss der Betreiber die Kassenpflicht-Erklärung abgeben — die Einschätzung, ob sein Betrieb unter den Umsatzgrenzen des § 131b BAO liegt, trifft er, nicht das Programm. SIMPro rechnet beide Grenzen laufend mit, warnt ab 80 % und zeigt den Stichtag; ab ihm bleiben Bar und Karte gesperrt, bis eine Signatureinheit eingerichtet ist. Die Signatur über fiskaltrust kommt mit Stufe 2 und setzt einen Vertrag voraus.
Zwei Sätze, die in diesem Zusammenhang oft untergehen: Eine Kartenzahlung vor Ort ist ein Barumsatz (§ 131b Abs 1 Z 3 BAO), auch ohne Bargeld. Und die Belegerteilungspflicht nach § 132a BAO hängt an keiner Umsatzgrenze — sie gilt auch für den kleinsten Betrieb ohne Registrierkasse.
11. Was der Server gegen Angriffe tut
Der Server ist aus dem Internet erreichbar; die Annahme ist durchgehend, dass er angegriffen wird. Ausführlich steht das in docs/SICHERHEIT.md — Bedrohungsmodell, jede Kontrolle mit Fundstelle im Quelltext, und ausdrücklich auch, wogegen sich das Programm nicht wehrt.
Was ein Betreiber davon wissen muss:
| Wogegen | Was greift |
|---|---|
| Passwörter durchprobieren | 5 Versuche / 15 min je IP, dazu eine Kontosperre, die sich mit jedem weiteren Fehlversuch verdoppelt (Deckel 24 h). fail2ban sperrt die IP nach gehäuften 401/429 |
| Ein abgefangener Zeitcode | Gilt genau einmal. Ein zweiter Versuch mit demselben Code wird abgelehnt |
| Ein Datenbankabzug | Passwörter und Wiederherstellungscodes als argon2id, TOTP-Geheimnisse AES-256-GCM, Erneuerungstoken nur als Hash. Ein Abzug gibt keine Sitzung her |
| Eine hochgeladene Datei | Das Logo wird aus den dekodierten Pixeln neu geschrieben. Was nicht Bild ist, überlebt das nicht — und EXIF samt GPS fällt weg |
| Zugriff über die Mandantengrenze | Zwei unabhängige Ebenen: der Anwendungsfilter und Row Level Security in der Datenbank, mit einer Rolle ohne BYPASSRLS |
Was ein Konto sperrt, hebt ein Administrator auf: Einstellungen → Betrieb → Benutzer und Rollen → Sperre aufheben. Eine Sperre, aus der nur die Zeit heraushilft, wäre die schlechtere Wahl — deshalb gibt es beides.
Bei jeder Kontosperre geht eine Mail an die Betreiberadresse.
12. Wenn etwas nicht funktioniert
| Symptom | Wahrscheinliche Ursache | Prüfen / Beheben |
|---|---|---|
| App: „Unter dieser Adresse antwortet kein Rechnungsserver" | Server aus, Container gestoppt, DynDNS zeigt auf alte IP, Portfreigabe fehlt | docker compose ps; /health im Browser; DynDNS-Status im Router |
| Zertifikatsfehler von außen | Let's Encrypt konnte Port 443 nicht erreichen | docker compose logs caddy-internal; Portfreigabe 443; zeigt die Domain auf die richtige IP? |
| Anmeldung geht, aber alles leer | Falscher Mandant oder falsche Rolle | Einstellungen → Konto; ein Administrator prüft die Mitgliedschaft |
| „Zu viele Versuche" | Drosselung: fünf Anmeldeversuche je 15 Minuten | Warten, oder ein Administrator hebt die Sperre unter Einstellungen → Betrieb → Benutzer und Rollen auf |
| E-Mails kommen nicht an | SMTP-Zugang falsch, Anbieter blockt | docker compose logs api nach „mail" filtern; Versandprotokoll am Beleg; Verwaltung → Server zeigt, ob SMTP überhaupt eingerichtet ist |
| Sicherungswarnung in der App | Speicher nicht erreichbar, Konto abgelaufen, Platte voll | docker compose logs backup; Plattenplatz; Konto beim Anbieter |
| Nach einem Server-Update geht die App nicht mehr | docker compose build vergessen, oder App zu alt | Abschnitt 7 vollständig ausführen; App aktualisieren |
| Container startet in Schleife neu | Fehler in .env (etwa Tippfehler in der Domain) | docker compose logs <name>; Einrichtungsskript erneut ausführen |
docker compose logs -f caddy bleibt leer | Der Dienst heißt im Modus BEHIND_PROXY caddy-internal | Mit dem richtigen Namen aufrufen |
Für alles, was hier nicht steht: die technische Dokumentation im Repository unter docs/ — INSTALL.md, NETWORK.md, BACKUP.md, RELEASE.md, STATUS.md — ist ausführlicher und wächst mit jeder Fassung.