/docs/archive
Unveränderliches Archiv
GoBD-konforme Aufbewahrung steuerrelevanter Dokumente nach § 146 AO — Hash-verkettet, zeitgestempelt, 10 Jahre revisionssicher.
Überblick
Das Invocore-Archiv ist eine unveränderliche Speicherschicht für jedes steuerrelevante Dokument. Beim Upload wird die Datei SHA-256-gehasht, an die organisationseigene Hash-Kette angehängt und auf das Dateisystem mit chmod 0444 + chattr +i geschrieben. Niemand — auch nicht Administratoren — kann archivierte Inhalte nachträglich ändern.
- ✓SHA-256 Hash pro Dokument, in Hash-Kette pro Organisation verankert
- ✓PostgreSQL-Trigger blockieren UPDATE / DELETE / TRUNCATE auf der Journaltabelle
- ✓Standard-Aufbewahrungsfrist 10 Jahre (§ 147 AO), nicht unterschreitbar
- ✓Stündliche Verankerung in Merkle-Tree mit RFC-3161 TSA-Zeitstempel
- ✓Self-contained ZIP-Verifikationspaket — externe Auditoren prüfen ohne Backend-Zugang
Rechtlicher Hintergrund
Die Grundsätze ordnungsmäßiger Buchführung in elektronischer Form (GoBD) verlangen drei Eigenschaften für jedes steuerrelevante Dokument:
§ 146 AO — Unveränderbarkeit: Einmal verbuchte Aufzeichnungen dürfen nicht in einer Weise verändert werden, dass der ursprüngliche Inhalt nicht mehr feststellbar ist. Wir setzen das mit PostgreSQL-Triggern (kein UPDATE / DELETE / TRUNCATE auf journal_entries) und Dateisystem-Immutability (chmod 0444 + chattr +i) durch.
§ 145 AO — Nachvollziehbarkeit: Jede Buchung muss mit ihrer Quelle und ihrem Urheber rekonstruierbar sein. Jeder Upload schreibt einen Audit-Log-Eintrag mit user_id, IP, SHA-256 und Block-Nummer.
§ 147 AO — Aufbewahrungsdauer: Buchungsbelege müssen 10 Jahre aufbewahrt werden. Wir setzen die Frist beim Upload, und der Service lehnt Überschreibungen unter 10 Jahre ab.
Hash-Kette pro Organisation
Jede Organisation startet mit einem deterministischen Genesis-Block (block_number = 0, prev_hash = 64×"0"). Jeder weitere Upload erzeugt einen neuen Block, dessen prev_hash auf den entry_hash des Vorgängers zeigt. Wird ein Dokument oder ein Block nachträglich geändert, bricht die Kette deterministisch ab einer bestimmten Stelle — die Integritätsprüfung meldet den Block-Index loud.
So entsteht ein Block
1. Hashing
Die Datei wird vollständig im Speicher SHA-256-gehasht. Falls bereits ein Block mit demselben Hash in der Organisation existiert, antwortet der Endpunkt mit 409 Conflict (Idempotenz nach Hash).
2. Speicherung
Die Bytes werden in das Primary-Storage (lokales Dateisystem mit immutability-Flag) geschrieben. Sekundäre Replikation (off-host) ist als Hetzner-Storage-Backend vorgesehen und über ARCHIVE_SECONDARY_ENABLED aktivierbar.
3. Block-Append
In derselben Transaktion wird ein neuer Datensatz in journal_entries angehängt. Eine PG-Trigger-Funktion (journal_entry_chain_trg) holt den entry_hash des letzten Blocks, schreibt ihn als prev_hash und berechnet den neuen entry_hash deterministisch aus block_number ‖ prev_hash ‖ doc_hash ‖ operation.
4. Audit-Log
Parallel wird ein audit_logs-Eintrag (action = archive_upload) mit user_id, IP, User-Agent, SHA-256 und Block-Nummer geschrieben. Rollback der Transaktion verwirft Block UND Audit-Log konsistent.
API-Referenz
Alle Endpunkte erfordern JWT-Authentifizierung (Bearer-Token) und sind mandantenisoliert über den X-Tenant-Id-Header.
/api/v1/archive/documentsDokument unveränderlich archivieren (Multipart-Upload). Liefert SHA-256, Block-Nummer und Hash-Verkettung.
Beispielantwort
{
"document_id": "31c2c1f0-502a-47f3-bb85-01f81362a7b7",
"sha256": "13ebe841cbed79bb6eec0031c3af5019…",
"block_number": 1,
"entry_hash": "61212e86d03e3165a15f958bf2975…",
"storage_primary_path": "42426bf6-1428-…/2026/05/31c2c1…",
"replication_status": "none",
"immutable_locked": true,
"retention_until": "2036-05-16T21:14:00Z"
}/api/v1/archive/documentsListe archivierter Dokumente filtern / sortieren. Felder: items, total, page, page_size, pages.
Beispielantwort
{
"items": [
{
"document_id": "…",
"original_filename": "rechnung_2026_001.pdf",
"sha256": "…",
"size_bytes": 4242
}
],
"total": 1,
"page": 1,
"page_size": 50,
"pages": 1
}/api/v1/archive/chain/verifyIntegrität der Hash-Kette für Ihre Organisation prüfen. ok=true bedeutet kein Block wurde verändert.
Beispielantwort
{
"ok": true,
"entries": 42,
"genesis": true,
"reason": null,
"broken_at": null
}/api/v1/archive/documents/{id}/verification_packageZIP herunterladen: Originaldatei + manifest.json + standalone verify.py + RFC-3161 TSA-Token.
Beispielantwort
{
"format_version": "1.0",
"document": {
"sha256": "…"
},
"chain": {
"entries": 42
}
}Aufbewahrungsfrist (§ 147 AO)
Die Aufbewahrungsfrist wird beim Upload als retention_until gesetzt (Standardwert: 10 Jahre für invoice, contract und form). Eine retention_years-Override unter 10 wird mit ValueError("archive.retention_too_short") abgelehnt — das ist der Compliance-Boden. Der nächtliche Retention-Sweep markiert abgelaufene Dokumente als is_retention_deleted = true, entfernt die Bytes von Primary + Secondary und schreibt einen unveränderlichen archive_retention_delete-Block in die Hash-Kette als Nachweis der Löschung.
TSA-Zeitstempel (RFC 3161)
Jede Stunde fasst der Anker-Worker alle neuen Blöcke pro Organisation in einem Merkle-Baum zusammen, berechnet die Wurzel und holt einen RFC-3161-Zeitstempel von einer Time Stamping Authority. Im Entwicklungsmodus ist das FreeTSA; für Produktion ist eine qualifizierte TSA (D-Trust, A-Trust) vorgesehen. Die TSA-Antwort wird als blob in der anchors-Tabelle gespeichert und im Verifikationspaket mitgeliefert. Sollte die TSA-Antwort fehlschlagen, wird der Anker mit exponentiellem Backoff erneut versucht — die Kette selbst ist bereits ohne den Zeitstempel integer.
Externe Verifikation
Jedes Dokument lässt sich vollständig offline verifizieren. Das ZIP-Verifikationspaket enthält alles, was ein externer Auditor braucht — kein Backend-Zugang erforderlich:
- →manifest.json mit Block-Nummer, SHA-256, prev_hash, entry_hash und Pfad zur Originaldatei
- →document/<filename> — die unveränderten Original-Bytes
- →verify.py — alleinstehendes Python-Script (nur stdlib + cryptography), das die SHA-256 prüft, die Kette neu berechnet und den TSA-Token gegen die Wurzel verifiziert
- →tsa_token.bin — RFC-3161-DER-Encoded Antwort der TSA, mit qualifiziertem Zertifikat
- →README.txt — Anleitung für Auditoren in deutscher Sprache
So sieht es im Konto aus
Das Archiv ist im Hauptmenü unter Archiv erreichbar. Jeder Mandant sieht ausschließlich die eigene Hash-Kette und die eigenen Dokumente.



Häufige Fragen
Was passiert, wenn ich dasselbe Dokument zweimal hochlade?
Der Server berechnet den SHA-256 und vergleicht ihn mit Ihrer Organisation. Bei einem Match antwortet er mit 409 Conflict und meldet den ursprünglichen Dateinamen. Sie können entscheiden, ob es ein versehentlicher Duplikat ist oder ob Sie die Originalkopie verifizieren möchten.
Kann ich ein archiviertes Dokument löschen?
Nicht regulär. Nur der nächtliche Retention-Sweep darf Dokumente nach Ablauf der 10-Jahres-Frist entfernen, und auch dann bleibt der Hash-Kettenblock — als Beweis, dass das Dokument einmal existiert hat. Für DSGVO-Löschpflichten greift derselbe Mechanismus.
Was, wenn die Hash-Kette bricht?
Der /chain/verify-Endpunkt liefert dann ok=false plus den Block-Index, an dem der Bruch auftritt. Das wäre ein Hinweis auf einen schwerwiegenden Verstoß (manuelle DB-Manipulation), und ein Operator kann anhand des audit_logs feststellen, wer den letzten gültigen Block schrieb und welcher Eintrag verändert wurde.
Wie wird das Archiv repliziert?
Primary-Speicher ist das lokale Dateisystem mit immutability-Flag. Sekundäre Replikation auf einen Hetzner-Storage-Box (off-host) ist über das ARCHIVE_SECONDARY_ENABLED-Flag aktivierbar. Bei aktivem Replikat sehen Sie pro Dokument einen Replikationsstatus (pending / replicated / failed).
Kann mein Steuerberater das Archiv direkt prüfen?
Ja. Sie laden das Verifikationspaket für die relevanten Dokumente herunter, übergeben es Ihrem Steuerberater, und er führt python verify.py auf einem beliebigen Rechner aus. Es wird kein Invocore-Zugang benötigt.
Bereit, das Archiv zu nutzen?
Das Archiv ist in jedem Invocore-Konto bereits aktiviert. Laden Sie das erste Dokument hoch — die Hash-Kette startet automatisch mit dem Genesis-Block.