AEYEZ: Dokumente fotografieren, Fristen erkennen

Problem
Behördenbriefe, Rechnungen, Verträge und Kassenbons kommen auf Papier, und die wichtige Information steckt in wenigen Feldern: Absender, Betrag, Frist. Ich wollte eine App, die aus einem Foto strukturierte Daten und Fristen macht. Der Nutzer sollte selbst entscheiden, welcher KI-Anbieter seine Dokumente sieht, bis hin zu einem lokalen Modell.
Meine Rolle
Ich habe Idee und Umfang festgelegt, die Design- und Umsetzungspläne abgenommen und den Code geprüft.
Vorgehen
Ein pnpm-Monorepo mit Expo-App (React Native, Android und Web), einem schlanken Next.js-API-Server auf Vercel und einer Landingpage. Dokumente und Seiten liegen in SQLite auf dem Gerät, API-Schlüssel im SecureStore. Pro Aufgabe (Analyse, Chat, Übersetzung, Ordnervorschlag) wählt der Nutzer Anbieter und Modell; der Client schickt die Wahl als Header mit, auf Wunsch auch den eigenen Schlüssel.
Die Analyse ruft generateObject aus dem Vercel AI SDK mit einem zod-Schema auf: Dokumenttyp, zehn Untertypen, typisierte Felder wie Betrag, Datum oder IBAN und Fristen mit Priorität. Für OpenRouter geht es über Text: JSON aus der Antwort lösen, Feldnamen normalisieren, dann dasselbe Schema prüfen lassen. Ollama und Hugging Face spricht die App direkt an, ohne Umweg über den Server.
Architektur
Erfassen
Kamera, Galerie oder PDF in der Expo-App, mehrere Seiten pro Dokument
Lokal speichern
SQLite auf dem Gerät, Schlüssel im SecureStore
AIClient
Wählt pro Aufgabe Anbieter und Modell. Lokale Anbieter direkt, Cloud über das Backend
Next.js-API auf Vercel
4 Routen: analyze, chat, translate, suggest-folders. Header x-ai-provider, x-ai-model, x-ai-key
Strukturierte Ausgabe
generateObject mit zod-Schema, bei OpenRouter Text plus Normalisierung plus zod.parse
App-Ansichten
Dokumente, Fristen, Chat mit Dokumenten, Einstellungen
Entscheidung
Ein Schema als Vertrag für alle Anbieter
Jeder Anbieter liefert JSON etwas anders, und nicht jeder kann strukturierte Ausgabe. Statt pro Anbieter einen Parser zu pflegen, ist das zod-Schema die einzige Wahrheit: generateObject, wo es geht, sonst Text, Normalisierung und analysisSchema.parse. Was das Schema nicht besteht, erreicht die App nicht. Der Preis: Das Schema ist streng, eine falsch benannte Kategorie lässt die ganze Analyse scheitern statt nur ein Feld.
Code-Auszug
const analysisSchema = z.object({
type: z.enum(['document', 'image', 'other']),
subtype: z.enum(['receipt', 'invoice', 'letter', 'gov_letter', 'form', 'contract', 'medical', 'id_document', 'certificate', 'bank']).nullable(),
confidence: z.number().min(0).max(1),
language: z.string().nullable(),
needs_translation: z.boolean(),
title: z.string().nullable(),
vendor: z.string().nullable(),
date: z.string().nullable(),
message: z.string(),
items: z.array(z.object({
item_key: z.string(),
item_value: z.string(),
value_type: z.enum(['string', 'number', 'currency', 'date', 'iban', 'boolean']),
category: z.enum(['line_item', 'meta', 'address', 'deadline', 'party', 'financial']),
confidence: z.number(),
})),
deadlines: z.array(z.object({
label: z.string(),
date: z.string(),
priority: z.enum(['low', 'normal', 'high', 'critical']),
sender_email: z.string().nullable(),
})),
});Ergebnisse
- 197Commits zwischen 08.03. und 14.04.2026
- 10Dokument-Untertypen im Schema, vom Kassenbon bis zum Behördenbrief
- 7wählbare Anbieter: 5 in der Cloud, 2 lokal
- 4API-Routen im Backend
Was ich daraus gelernt habe
Der Server springt mit eigenen Schlüsseln ein, wenn der Nutzer keinen mitschickt, die API hat kein Login, und CORS steht auf *. Vor jedem öffentlichen Test hätte ich zuerst Auth und ein Limit gebraucht. Außerdem habe ich zu früh in die Breite gebaut: ein Modul-System mit Kauf-Tracking und einen Scraper. Modul-Registry und Scraper habe ich im April wieder entfernt, über 1.100 Zeilen. Und es gibt keine automatisierten Tests, obwohl gerade die Normalisierung der Modellantworten welche gebraucht hätte.