Diese Dokumentation beschreibt die technische Architektur des Datenformats für die Verwendung außerhalb der Receipts Space App. Das Design folgt den Prinzipien von “Local First” und “File Over App”.
Highlights
- Offline-First: Vollständig ohne Server nutzbar
- Konfliktfrei: CRDT-basierte Synchronisation über beliebige Speichermedien
- Manipulationssicher: Verkettete Transaktionen mit optionaler Signierung
- Langlebig: Einfaches, dokumentiertes Format (JSON/JSONL)
- Portabel: Daten gehören dem Nutzer, nicht der App
Beispiel-Code auf Github
Verzeichnisstruktur
Ein Workspace ist wie folgt aufgebaut:
workspace/
├── info.json → Metadaten
├── transactions/<clientId>/<...>/<idx>.dat → Änderungslog
└── assets/<clientId>/<...>/<idx>.dat → DateianhängeWorkspace-Metadaten (info.json)
| Feld | Beschreibung |
|---|---|
apiVersion | Format-Version, aktuell 3 |
workspaceType | Immer receipts2 für Receipts Space |
workspaceId | Eindeutige ID des Workspaces |
createDate | Erstellungszeitpunkt (Unix-Timestamp in Sekunden) |
encryption | (optional) Verschlüsselungskonfiguration → Verschlüsselung |
Transaktionen
Änderungen werden in einem fortlaufenden Log gespeichert. Jeder Client führt sein eigenes Log, identifiziert durch eine eindeutige clientId. Die Einträge werden fortlaufend nummeriert (0.dat, 1.dat, …) und zur Vermeidung von Dateisystem-Konflikten nach einem Verteilungs-Algorithmus in Unterverzeichnisse aufgeteilt (max. 1000 Dateien pro Ordner).
Konfliktfreie Synchronisation (CRDT)
Jeder Datenbankeintrag enthält:
| Feld | Beschreibung |
|---|---|
_id | Eindeutige ID des Eintrags |
_type | Datentyp (z.B. category, receipt) |
_v | Versionszähler (Lamport-Clock) |
Die Synchronisation nutzt Last-Write-Wins (LWW): Ein Eintrag wird nur übernommen, wenn sein _v größer ist als der vorhandene Wert. Bei gleichem _v entscheidet der Zeitstempel. Dadurch ist die Reihenfolge der Anwendung irrelevant (CRDT).
Dateiformat
Jede Transaktionsdatei besteht aus zwei Teilen, getrennt durch einen Zeilenumbruch:
- Header (erste Zeile): JSON-Objekt mit Metadaten
- Content (Rest): JSONL-formatierte Änderungen (UTF-8)
Header-Felder:
| Feld | Beschreibung |
|---|---|
s | Content-Größe in Bytes |
c | SHA256-Checksum des Contents (URL-safe Base64, ohne Padding) |
t | Erstellungszeit (Unix-Timestamp in Sekunden) |
v | Format-Version, aktuell 1 |
p | (optional) Hash der vorherigen Transaktion → Verkettung |
did | (optional) Eindeutige Device-ID der Installation |
Beispiel:
{"c":"Sg96RWfbFLN6_d3gsG65IJjJVgM9Jw9yFbxSzaYrxv8","did":"8rc37rrax970579qkvra65cke4","p":"za_9iYEZLZtTmdv0MMaV_IwD_tF17Sx-imb31KhkgUY","s":4134,"t":1768137121,"v":1}
{"_id":"47855a70de36449f821d40b45f8c170a","_type":"category","_v":1,"title":"Telekommunikation"}Device-ID
Die Device-ID did ist eine eindeutige Kennung pro Installation (Gerät oder Benutzer). Sie wird in der ersten Transaktion eines Clients gesetzt. Dieselbe did kann von mehreren Clients gemeinsam verwendet werden, aber jeder Client hat immer nur eine did.
Transaktionsverkettung
Über das optionale Feld p (previous) wird jede Transaktion mit ihrer Vorgängerin verknüpft. Der Wert ist der SHA256-Hash der kompletten vorherigen Datei (Header + Content).
Bei verschlüsselten Workspaces wird der Hash über die verschlüsselte Datei gebildet (IV + Ciphertext + AuthTag), nicht über den Klartext.
┌────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ 0.dat │◄────│ 1.dat │◄────│ 2.dat │
│ p: hash(info.json) │ p │ p: hash(0) │ p │ p: hash(1) │
└────────────────────┘ └──────────────┘ └──────────────┘Garantien:
- Integrität: Manipulation ändert den Hash → Kettenbruch erkennbar
- Vollständigkeit: Fehlende Transaktionen brechen die Kette
- Reihenfolge: Chronologie ist kryptografisch gesichert
Verifizierung: Von der letzten Transaktion rückwärts prüfen, ob jeder p-Wert mit dem berechneten Hash der Vorgänger-Datei übereinstimmt.
Dateianhänge (Assets)
Binärdateien werden separat als Assets gespeichert, analog zu Transaktionen mit fortlaufendem Index pro Client.
Asset-URL Format: asset:///<clientId>/<index>/<filename>?s=<size>&t=<mimeType>&d=<checksum>
| Parameter | Beschreibung |
|---|---|
clientId | Client-ID |
index | Fortlaufender Index |
filename | Originaler Dateiname |
s | Größe in Bytes |
t | MIME-Type |
d | SHA256-Checksum (Base64) |
Beispiel: asset:///1EH7BEtuL9xOz5aTpEyI4K/466/rechnung.pdf?s=6284&t=application%2Fpdf&d=JAd0HmXcSIVVdYMmDBjfVZeTvAyXQ94GmjA6CwSwOYU
Die Metadaten sind Teil der URL, wodurch die Integrität beim Laden geprüft werden kann.
Datentypen
| Typ | Format | Beispiel |
|---|---|---|
| ID / Referenz | _id des Zielobjekts | "category": "abc123" |
| Datum | Integer YYYYMMDD | 20241201 (1. Dez 2024) |
| Betrag | Fließkommazahl | 1.23 |
| Timestamp | Unix-Sekunden | 1732311704 |
Bei den einzelnen Datenfeldern sind einige Besonderheiten zu beachten:
- Der Dokumententyp ist in
doctypeabgelegt. Aktuell existiert nur ein Dokumententyp mit der IDd0c5d0c5d0c5d0c5d0c5d0c5d0c5d0c5. Ist dieser nicht gesetzt, wird anhand des Wertes increditentschieden, ob es sich um eine Einnahme (true) oder eine Ausgabe (false) handelt. - Kontakt (
contact) und Kategorie (category) werden durch die ID referenziert. - Die Schlagwörter in
tagssind als Dictionary abgelegt, wobei die Keys die Referenz / ID darstellen und die Values anzeigen, ob der Wert gesetzt (truthy) oder gelöscht (falsy) wurde. - Die Steuersumme wird in
taxabgelegt. Die Steuer-Details werden intaxDetailsabgelegt. Auch hier wird ein Dictionary genutzt. Die Keys stellen den Steuersatz als String dar (z. B."19.0") und die Values den Betrag. - Der Abschluss, der im revisionssicheren Modus auch zum Schreibschutz führt, ist in
confirmedabgelegt.
Verschlüsselung (optional)
Für sensible Daten werden Transaktionen und Assets vollständig verschlüsselt (Header + Content). Die Verschlüsselung wird workspace-weit in info.json konfiguriert – ein Workspace ist entweder komplett verschlüsselt oder gar nicht.
Konfiguration in info.json
{
"apiVersion": 3,
"workspaceType": "receipts2",
"workspaceId": "...",
"createDate": 1732311704,
"encryption": {
"algorithm": "aes-256-gcm",
"kdf": "pbkdf2",
"kdfHash": "sha256",
"kdfIterations": 100000,
"salt": "base64-random-salt-16bytes",
"verify": "base64-encrypted-test-string"
}
}| Feld | Beschreibung |
|---|---|
algorithm | Verschlüsselungsalgorithmus: aes-256-gcm |
kdf | Schlüsselableitungsfunktion: pbkdf2 |
kdfHash | Hash-Funktion für PBKDF2: sha256 |
kdfIterations | Anzahl Iterationen (min. 100.000 empfohlen) |
salt | Zufälliger Salt (Base64, min. 16 Bytes) |
verify | Verschlüsselter Teststring zur Passwort-Prüfung |
Schlüsselableitung
Passwort + Salt → PBKDF2-SHA256 (kdfIterations) → Workspace-Key (256 Bit)Passwort-Verifikation
Das Feld verify enthält den String receipts2 verschlüsselt mit dem Workspace-Key:
verify = [IV (12B)][Ciphertext + AuthTag (16B)]Prüfung: Entschlüsselung von verify → Ergebnis = receipts2? → Passwort korrekt ✓
Dateistruktur (verschlüsselt)
Transaktionen und Assets werden vollständig verschlüsselt:
[IV (12B)][Ciphertext + AuthTag (16B)]
Ciphertext = AES-GCM(Header + "\n" + Content)Verschlüsselungsablauf (AES-256-GCM)
Verschlüsseln:
- Erstelle normale Transaktionsdatei (Header + Content)
- Generiere zufällige IV (12 Bytes)
- Verschlüssele:
encrypted = AES-GCM(key, iv, plaintext) - Speichere:
[IV (12B)][encrypted](enthält Ciphertext + AuthTag)
Entschlüsseln:
- Extrahiere IV (erste 12 Bytes)
- Extrahiere
encrypted(Rest, enthält Ciphertext + AuthTag) - Entschlüssele:
plaintext = AES-GCM-Decrypt(key, iv, encrypted) - Parse Header (erste Zeile) und Content (Rest)
- Prüfe Checksum
cgegen Content
Sicherheitshinweise
| Aspekt | Empfehlung |
|---|---|
| Algorithmus | AES-256-GCM (authentifizierte Verschlüsselung) |
| IV | Niemals wiederverwenden, immer zufällig generieren (12 Bytes) |
| Schlüsselableitung | PBKDF2 mit min. 100.000 Iterationen |
| Salt | Einmalig pro Workspace generieren, in info.json speichern |
| Passwort-Prüfung | AES-GCM AuthTag schlägt bei falschem Passwort fehl |
Einschränkungen
Bei vollständiger Verschlüsselung:
- Sync erfordert Passwort: Ohne Passwort sind keine Metadaten lesbar
- Keine Kettenverifizierung ohne Passwort:
pliegt im verschlüsselten Header - Dateiname bleibt sichtbar: Nur der Inhalt ist geschützt
Hinweis
Bei aktivierter Verschlüsselung ist die gesamte Transaktion ohne das korrekte Passwort nicht lesbar. Die Integritätsprüfung über c erfolgt nach der Entschlüsselung.