/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

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. 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. 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. 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. 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.

POST/api/v1/archive/documents

Dokument 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"
}
GET/api/v1/archive/documents

Liste 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
}
GET/api/v1/archive/chain/verify

Integritä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
}
GET/api/v1/archive/documents/{id}/verification_package

ZIP 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.

Archivseite mit Hash-Kettenstatus und Liste archivierter Dokumente
Übersichtsseite mit Hash-Kette ("Intakt"), Replikationsstatus und Liste der archivierten Dokumente.
Drag-und-Drop-Bereich für Datei-Upload
Upload-Bereich — PDF, PNG oder JPEG per Drag-and-Drop oder Klick. 10 Jahre Aufbewahrung bei jedem Upload bestätigt.
Download-Button für das Verifikationspaket
Pro Zeile ein Download-Button für das ZIP-Verifikationspaket — ideal für Steuerberater-Anhänge.

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.