## 01 Bestandsaufnahme

Das ist heute schon da — und es ist mehr, als für eine iOS-App nötig wäre. Der Plan unten baut bewusst darauf auf, statt etwas Neues danebenzustellen.

Bereich | Ist-Zustand
--- | ---
Frontend | Next.js 15 (App Router), React 19, TypeScript, Tailwind — als Website ausgeliefert, kein Mobile-Client.
Backend | Dieselbe Next.js-App: REST-artige JSON-Endpunkte unter `/api/*` für Auth, Scan, Receipts, Projects, Export, Billing.
Datenbank | PostgreSQL via Drizzle ORM. Tabellen für `users`, `sessions`, `receipts`, `projects`, `licenses`, `security_events` existieren bereits vollständig.
Auth | Selbstgebaut: E-Mail/Passwort + Google OAuth. Server-seitige Sessions (Token in httpOnly-Cookie, nur Hash in der DB) plus CSRF-Double-Submit-Cookie für Browser-Requests.
Abrechnung | Stripe Checkout — Wochen-Pass (4,99 €, Abo), Jahres-Pass (39,99 €, Abo), Lifetime (59,99 €, Einmalzahlung). Webhook setzt `users.plan` / `expiresAt`.
Beleg-Speicherung | Zwei Modi: nicht angemeldet → lokal im Browser (IndexedDB, „Guest Bucket“). Angemeldet → serverseitig in Postgres, pro `userId` isoliert. Das ist der Teil, der die iOS-App trägt.
KI-Extraktion | Serverseitig via Vercel AI SDK (OpenAI / Google) — Bild rein, strukturierte Beleg-Daten raus. Läuft schon rein serverseitig, ein iOS-Client müsste nur ein Bild hochladen.
Export | Dual-Sheet XLSX (ExcelJS), DATEV-CSV, PDF — serverseitig generiert.
Deployment | Docker Compose (App + Postgres), nginx davor, eigenes Least-Privilege-DB-Rollenmodell, Security-Hardening bereits durchgeführt.
Rechtstexte | Impressum/Datenschutz/AGB sind aktuell US-Templates ohne DSGVO-Inhalt — bekannte Lücke, relevant auch für den App-Store-Review (siehe Abschnitt 8).

Wichtigster Befund

Es gibt schon eine vollständige, mandantenfähige REST-API mit Session-Auth, Stripe-Billing und Postgres-Persistenz pro Nutzer. Eine iOS-App muss kein neues Backend bekommen — sie wird ein zweiter Client für das bestehende. Der Datenbank-Teil der Aufgabe („gleicher Pro-Account auf Web und App“) ist damit strukturell schon gelöst, sobald die App sich gegen dieselbe API authentifiziert.

## 02 Kann / kann nicht / soll / soll nicht

Kurz eingeordnet, damit der iOS-Scope nicht zufällig größer wird als die Web-App selbst.

### Kann heute (Web)

- Belege per Foto/Scan/PDF hochladen, KI-Extraktion (Händler, Betrag, MwSt., Position, Kategorie).
- Beleg-Review mit Bounding-Box-Abgleich, manuelle Korrektur, Line-Items editieren.
- Ordner/Projekte (Pro), Filter, Volltextsuche, Bulk-Aktionen.
- Export als XLSX (Dual-Sheet), DATEV-CSV, PDF.
- Account, Login, Passwort-Reset, Google-Login, Pro-Abo via Stripe.
- Admin-Dashboard (intern, nicht kundenrelevant).

### Kann nicht / bewusst nicht

- Keine Offline-Erst-Nutzung für angemeldete Nutzer — Belege eingeloggter Accounts leben serverseitig, nicht lokal.
- Keine automatisierte Buchhaltungs-Anbindung (DATEV-Direktexport ist eine Datei, kein API-Sync).
- Keine Team-/Mehrbenutzer-Konten — ein Account = ein Nutzer.

### Soll die iOS-App

- Dieselben Belege, denselben Account, denselben Pro-Status zeigen wie die Web-App — synchron, nicht als Kopie.
- Die native Stärke von iOS nutzen, die die Website nicht hat: Kamera-Dokumentenscanner (VisionKit) statt Datei-Upload, Share-Sheet-Integration („Beleg direkt aus Mail/Fotos an ScanReceipts schicken“), Face ID/Touch ID zum App-Öffnen, Push-Benachrichtigung bei fertiger Extraktion.
- Denselben Funktionsumfang wie das Web-Dashboard abbilden: Übersicht, Review, Export, Einstellungen, Abo-Verwaltung.

### Soll die iOS-App nicht

- Kein eigenständiges Produkt mit eigener Feature-Roadmap — sie folgt der Web-App, nicht umgekehrt.
- Kein zweites Backend, keine zweite Datenbank, keine Datenhaltung, die aus dem Sync herausfällt.

## 03 Technologie-Entscheidung

Drei realistische Wege für den Client. Die Wahl bestimmt Aufwand, App-Store-Wahrnehmung und wie viel vom bestehenden React-Code wiederverwendbar ist.

Ansatz | Aufwand | iOS-Gefühl | Code-Wiederverwendung | Kamera-Scan-Qualität
--- | --- | --- | --- | ---
Nativ, Swift/SwiftUI | Hoch (neuer Code) | Bestmöglich | Keine (nur die API-Verträge) | VisionKit nativ, beste Qualität
React Native / Expo | Mittel | Gut, mit Feinschliff | Logik/Hooks ja, UI-Komponenten nein (kein DOM) | Über Community-Module, weniger nativ
Capacitor (Web-App im Wrapper) | Niedrig | Schwach — fühlt sich wie Website an | Fast alles | Nur über Plugin, Web-Kamera-API als Fallback

Empfehlung

Natives SwiftUI für den Client, mit dem bestehenden Next.js-Backend als reiner API-Lieferant. Begründung: ScanReceipts lebt vom Dokumentenscan — genau da ist der Unterschied zwischen einer nativen VisionKit-Kamera und einer Web-Kamera-API am größten spürbar. Zusätzlich bewertet Apples Review nativ gebaute Apps im Zweifel wohlwollender, und ein Finanz-/Beleg-Produkt sollte Face-ID-Schutz, Keychain-Speicherung und ordentliches Offline-Verhalten haben — Dinge, die in SwiftUI Bordmittel sind und in einem Wrapper immer ein Stück Umweg bleiben. Der Mehraufwand gegenüber Capacitor zahlt sich hier aus, weil das Produkt kein einfaches Formular ist, sondern seine Existenzberechtigung aus Kamera + KI-Review zieht.

Realistische Alternative, falls Zeit/Budget der limitierende Faktor ist: Capacitor als schneller erster Wurf, um früh im App Store zu stehen, mit dem Plan, die Kamera- und Review-Screens später nativ nachzuziehen. Das ist ein legitimer Kompromiss — sollte aber als bewusste Zwischenstufe behandelt werden, nicht als Endzustand, sonst bleibt die App dauerhaft bei „fühlt sich wie eine Website an“ stehen.

## 04 Der harte Punkt: Apple In-App-Purchase

Das ist der Teil des Plans, der die Web-Logik nicht einfach übernehmen kann — und der beim App-Store-Review scheitert, wenn er übergangen wird.

Apple Guideline 3.1.1 — Pflicht, kein Nice-to-have

Digitale Inhalte/Funktionen, die innerhalb der iOS-App freigeschaltet werden — hier: der Pro-Status, der mehr Scans, Ordner und Export freischaltet — müssen über Apple In-App Purchase (StoreKit) verkauft werden, sobald der Kauf aus der App heraus angestoßen wird. Ein Stripe-Checkout-Link, der in der App geöffnet wird, führt zur Ablehnung im Review. Die „Reader-App“-Ausnahme (die z. B. Spotify/Netflix nutzen, um extern zu verkaufen) greift nur für Apps, deren Kerninhalt außerhalb der App erworben und in der App nur konsumiert wird — ein Belegscanner mit Nutzungslimits erfüllt dieses Kriterium nicht.

### Was das konkret bedeutet

- Für den iOS-Kauf braucht es StoreKit 2 mit eigenen Produkten im App Store Connect — die bestehenden Stripe-Preise (Wochen-Pass, Jahres-Pass, Lifetime) müssen als Apple-Abos/In-App-Kauf gespiegelt werden. Preise dürfen unterschiedlich sein (Apple hat eigene Preisstufen und behält bis zu 30 % / 15 % im Small-Business-Programm ein) — das ist im Pricing einzuplanen, nicht kosmetisch.
- Der Server muss iOS-Käufe genauso erkennen wie Stripe-Käufe: Apple schickt serverseitige App Store Server Notifications V2 (das iOS-Äquivalent zum Stripe-Webhook). Ein neuer Endpunkt `/api/webhooks/apple` validiert die JWS-signierte Notification und setzt `users.plan`/`expiresAt` — dieselbe Funktion (`isProActive`), die heute schon für Stripe existiert, bedient dann beide Quellen.
- Ein Nutzer, der auf der Website mit Stripe zahlt, muss in der App sofort als Pro erkannt werden (und umgekehrt) — das ist automatisch der Fall, weil beide Wege am Ende nur `users.plan` in derselben Zeile setzen. Es gibt keinen Bedarf, zwei Kaufwege gegeneinander zu synchronisieren; sie schreiben ins selbe Feld.
- Empfehlung fürs Onboarding in der App: Bereits-Web-Kunden loggen sich einfach mit ihrem bestehenden Account ein und sind sofort Pro — kein Kauf in der App nötig. Der In-App-Kauf ist nur für Leute relevant, die zum ersten Mal in der App kaufen wollen.
- Ein optionales Spalten-Add-on im Schema (`purchase_source: 'stripe' | 'apple'`) hilft später bei Support/Kündigung, ist aber kein Launch-Blocker.

### Zweiter Pflichtpunkt: Sign in with Apple

Weil die App Google-Login anbietet (bzw. anbieten würde), verlangt Guideline 4.8, dass parallel auch „Sign in with Apple“ angeboten wird. E-Mail/Passwort-Login allein reicht als Ausweg nicht, sobald ein Drittanbieter-Login existiert. Technisch ist das ein weiterer OAuth-ähnlicher Provider neben dem bestehenden `oauth_accounts`-Mechanismus (aktuell nur `provider: 'google'`) — Aufwand ist überschaubar, weil die Tabelle dafür schon vorgesehen ist.

## 05 Ein Backend, zwei Clients

Was am Next.js-Backend geändert werden muss, damit es einen nativen App-Client genauso sauber bedient wie den Browser — ohne die Web-Seite zu verändern.

### 5.1 Auth: Bearer-Token statt Cookie für die App

Die Web-App nutzt einen httpOnly-Session-Cookie plus ein CSRF-Double-Submit-Cookie (`sr_session` / `sr_csrf`) — beides ist ein Browser-Mechanismus und funktioniert in einer nativen App nicht (kein Cookie-Jar, kein DOM zum Auslesen des CSRF-Cookies). Für native Clients braucht es einen zweiten, parallelen Auth-Pfad, ohne den bestehenden zu verändern:

- Login/Signup-Endpunkte erkennen einen App-Client (z. B. Header `X-Client: ios`) und geben das Session-Token zusätzlich im JSON-Body zurück statt nur als Set-Cookie.
- Die App speichert das Token in der iOS Keychain (nicht UserDefaults) und schickt es als `Authorization: Bearer <token>`.
- `getCurrentUser()` in `src/lib/auth/session.ts` liest zusätzlich den Authorization-Header, nicht nur das Cookie — dieselbe `sessions`-Tabelle, derselbe Hash-Vergleich, nur eine zweite Quelle für den Token.
- CSRF-Pflicht (`requireCsrf`) gilt nur für Cookie-authentifizierte Requests — ein Request mit gültigem Bearer-Token ist per Definition kein Cross-Site-Request eines fremden Browsers und braucht den Double-Submit-Schutz nicht. Das ist eine kleine, gut abgegrenzte Änderung an einer einzigen Funktion, keine Umstrukturierung.

Warum das reicht

CORS und die Origin-Prüfung in `isAllowedOrigin` betreffen ausschließlich Browser — eine native App sendet keinen `Origin`-Header und ist von dieser Prüfung ohnehin nicht betroffen (der Code behandelt „kein Origin-Header“ schon heute als Server-zu-Server-Fall). Es muss an der Web-Sicherheit nichts gelockert werden, damit die App funktioniert.

### 5.2 Scan-Endpunkt bleibt, Upload-Quelle ändert sich

`POST /api/scan` nimmt heute schon `multipart/form-data` mit einer Bild-/PDF-Datei entgegen und braucht eine verifizierte Session. Die App liefert statt einer Browser-`File` ein per VisionKit gescanntes Bild (JPEG) — der Endpunkt selbst ändert sich nicht, nur wer ihn aufruft.

### 5.3 Push-Benachrichtigungen (optional, sinnvoll)

Die KI-Extraktion läuft serverseitig und dauert ein paar Sekunden. Für die App lohnt sich ein `device_tokens`-Tabelle (userId → APNs-Token) plus ein Push nach fertiger Extraktion — kein Muss für Version 1, aber eine kleine, klar abgegrenzte Erweiterung, kein Umbau.

### 5.4 Was sich nicht ändert

- Datenmodell (`users`, `receipts`, `projects`, `line_items`) — 1:1 wiederverwendbar, kein neues Schema.
- `/api/receipts`, `/api/projects`, `/api/export/*` — unverändert nutzbar, sobald die App authentifiziert ist.
- Die KI-Extraktionslogik, die Stripe-Web-Zahlung, das Admin-Dashboard.

## 06 Feature-Parität: Web → iOS

Web-Feature | iOS-Umsetzung | Status
--- | --- | ---
Datei-Upload / Dropzone | VisionKit-Dokumentenscanner (`VNDocumentCameraViewController`) + Fotobibliothek + Dateien-App | Neu (nativ)
Dual-Pane Review-Modal | Eigener SwiftUI-Screen: Bild oben/seitlich, Felder darunter — Layout an schmalen Screen angepasst statt 50/50-Split | Neu (nativ)
Line-Items-Editor | Native Liste mit Swipe-to-Delete, Inline-Edit | Neu (nativ)
LiveTable / Belegübersicht | Native Liste (`List`/`LazyVStack`) mit denselben Statusbadges | Neu (nativ)
Filter-Chips, Suche, Bulk-Aktionen | Gleiche Logik, native Controls | Neu (nativ)
Export XLSX/CSV/PDF | Ruft denselben `/api/export/*`-Endpunkt auf, zeigt iOS-Share-Sheet zum Speichern/Versenden | API wiederverwendbar
Ordner/Projekte (Pro) | Native Ordneransicht über `/api/projects` | API wiederverwendbar
Login / Signup / Passwort-Reset | Native Formulare gegen bestehende `/api/auth/*`-Endpunkte, plus Sign in with Apple | API + Apple-Login nötig
Pro-Abo abschließen | StoreKit 2 statt Stripe Checkout — siehe Abschnitt 4 | Muss neu gebaut werden
Abo verwalten/kündigen | Verweis auf iOS-Systemeinstellungen (Apple verwaltet In-App-Abos zentral) für App-Käufe; bestehender Web-Flow bleibt für Stripe-Käufe | Zwei Pfade, je nach Kaufquelle
Admin-Dashboard | Kein iOS-Äquivalent nötig — bleibt Web-only | Out of scope

## 07 Phasenplan

Sechs Phasen, jede für sich abnahmefähig. Aufwandsangaben sind grobe Orientierung für eine Einzelperson bzw. ein kleines Team, kein Fixpreis-Angebot.

#### Phase 0 — Vorbereitung

~1 Woche

- Apple Developer Program Account anlegen (99 $/Jahr), Bundle-ID, App-Store-Connect-Eintrag.
- Xcode-Projekt aufsetzen, SwiftUI-App-Grundgerüst, API-Client-Layer (URLSession + Codable, gegen die bestehenden JSON-Verträge).
- Backend: Bearer-Token-Auth-Pfad (Abschnitt 5.1) implementieren und gegen die App testen — parallel zum bestehenden Cookie-Flow, ohne ihn zu verändern.

#### Phase 1 — Auth & Account

~1–1,5 Wochen

- Login, Signup, Passwort-Reset, E-Mail-Verifizierung (Deep-Link/Universal-Link zurück in die App).
- Sign in with Apple (Pflicht wegen Google-Login, Abschnitt 4).
- Face-ID/Touch-ID-Sperre für App-Öffnen (Nice-to-have, aber bei Finanzdaten naheliegend).

#### Phase 2 — Scannen & Review

~2–3 Wochen

- VisionKit-Dokumentenscanner, Upload an `/api/scan`, Fortschrittsanzeige.
- Review-Screen: extrahierte Felder anzeigen/korrigieren, Line-Items editieren, speichern.
- Belegübersicht (Liste), Status-Badges, Suche/Filter.

#### Phase 3 — Pro-Kauf (StoreKit)

~1,5–2 Wochen

- Produkte in App Store Connect anlegen (Wochen-/Jahres-Abo, Lifetime als Non-Consumable).
- StoreKit-2-Kaufabwicklung in der App.
- `/api/webhooks/apple` für App Store Server Notifications V2, verknüpft mit `isProActive`-Logik.
- Paywall-Screen, Restore-Purchases-Flow.

#### Phase 4 — Export & Ordner

~1 Woche

- Export-Screen gegen bestehende `/api/export/*`-Endpunkte, iOS-Share-Sheet.
- Ordner/Projekte-Verwaltung (Pro-Feature) gegen `/api/projects`.

#### Phase 5 — App-Store-Freigabe

~1–2 Wochen (inkl. Review-Wartezeit)

- DSGVO-konforme Datenschutzerklärung + App Privacy „Nutrition Label“ in App Store Connect (siehe Abschnitt 8 — Blocker, unabhängig von der App selbst).
- Screenshots, App-Store-Text, TestFlight-Beta mit echten Testern.
- Review-Einreichung, typischerweise 1–3 Tage Wartezeit, ggf. Nachbesserung bei Rejection.

## 08 Rechtliches & App-Store-Freigabe

Dinge, die unabhängig vom Code erledigt sein müssen, bevor Apple die App überhaupt annimmt.

Bekannte Lücke, jetzt relevant

Die aktuellen Rechtstexte (Impressum, Datenschutz, AGB) sind US-Templates ohne DSGVO-Inhalt. Für die Website ist das ein Launch-Blocker; für den App-Store-Review ist es das ebenfalls — App Store Connect verlangt eine echte, erreichbare Datenschutz-URL, und deren Inhalt muss mit dem übereinstimmen, was im „App Privacy“-Fragebogen angegeben wird (welche Daten werden erhoben: E-Mail, Zahlungsdaten via Apple, Beleg-/Finanzdaten, Standort falls genutzt). Diese Arbeit sollte vor Phase 5 laufen, nicht danach.

- Apple Developer Program — 99 $/Jahr, Einzelperson oder Organisation (D-U-N-S-Nummer bei Organisation, dauert erfahrungsgemäß am längsten — früh beantragen).
- App Privacy Nutrition Label — Katalog aller erhobenen Datentypen (Kontakt, Finanzdaten, Nutzungsdaten) je Verwendungszweck; muss zum tatsächlichen Datenfluss passen (Belegbilder + extrahierte Beträge sind „Finanzdaten“).
- Export-Compliance — Standard-HTTPS/TLS-Verschlüsselung ist deklarationspflichtig, aber unkritisch (Standardformular in App Store Connect).
- Kassenbon-/Steuerdaten aus Deutschland — keine App-Store-spezifische Anforderung, aber die DSGVO-Verarbeitung (Auftragsverarbeitungsvertrag mit dem Hosting, Löschkonzept) sollte ohnehin für die Website nachgezogen werden; die App teilt sich dasselbe Backend und damit dieselbe Rechtsgrundlage.
- Impressumspflicht — in der App selbst (nicht nur auf der Website verlinkt) empfehlenswert, da deutsches Recht (TMG/DDG) Diensteanbieter-Kennzeichnung auch für Apps nahelegt.

## 09 Offene Entscheidungen

Punkte, die vor oder während Phase 0 eine bewusste Entscheidung brauchen — keine davon blockiert den Start, aber alle beeinflussen den Zuschnitt.

→Preisparität App vs. Web: Gleiche Preise trotz Apples Provision (30 % / 15 % im Small-Business-Programm) oder App-Preise leicht anheben, um die Marge zu halten? Viele Apps lösen das mit leicht höheren In-App-Preisen.
→Android parallel oder später? Der Plan hier ist iOS-spezifisch; eine spätere Android-Version bräuchte denselben Bearer-Token-Auth-Pfad plus Google-Play-Billing statt StoreKit — das Backend-Muster aus Abschnitt 5 trägt beides.
→Offline-Fähigkeit in der App: Soll die App wie die Web-Version einen anonymen Lokal-Modus haben, oder ist Login von Anfang an Pflicht? Login-Pflicht vereinfacht Phase 1–2 spürbar.
→React Native/Expo statt nativ: Falls Time-to-Market wichtiger ist als natives Gefühl, ist das eine valide Umentscheidung — würde vor allem Phase 2–4 betreffen, Abschnitt 4 und 5 blieben unverändert gültig.

## 10 Nächster Schritt

Der kleinste sinnvolle erste Schritt, der noch keine App-Store-Kosten oder Xcode-Setup voraussetzt: den Bearer-Token-Auth-Pfad aus Abschnitt 5.1 im bestehenden Next.js-Backend bauen und mit `curl`/Postman durchtesten. Das ist eine in sich geschlossene, risikoarme Änderung, die die Web-App nicht berührt — und sie ist die Voraussetzung für praktisch jeden weiteren Schritt in diesem Plan.