# CLAUDE_backend.md — Mini-SDR Backend

Bindende Zusatz-Konvention für alle **Backend-Arbeit** im Mini-SDR: API-Routes,
Server-seitige Logik, Supabase-Zugriff, Auth/Session, Fehlerbehandlung.

Diese Datei ergänzt die Projekt-`CLAUDE.md` (gemeinsame Regeln) und
`CLAUDE_frontend.md` (UI). Bei Konflikt gilt für reine Backend-Belange diese
Datei. Geltungsbereich: `app/api/`, `lib/` (außer rein Client-seitige Teile),
`middleware.ts`, `app/**/route.ts`, Server-Components-Datenladen.

---

## 0. Herkunft: 1:1 aus SDRv4

Auch die Mechanik kommt aus SDRv4 (`D:/Devel/NodeJS/SDRv4/`). Der Auth-Flow,
das Error-Wrapper-Muster und der Supabase-Zugriff folgen SDRv4. Bei Unklarheit
zuerst die SDRv4-Route lesen (`app/api/.../route.ts`), dann portieren. Unterschied:
SDRv4 importiert `withErrorHandler`/`AuthenticationError` aus `@murc134/core`,
der Workspace hat eine eigene, gleichwertige Implementierung in `lib/api-handler.ts`.

---

## 1. API-Route-Muster (CRITICAL)

Jede Route wird mit `withErrorHandler` aus `lib/api-handler.ts` umschlossen:

```ts
import { withErrorHandler, AuthenticationError, ValidationError } from '@/lib/api-handler'

export const POST = withErrorHandler(async (request) => {
  // ... wirft AppError-Subklassen bei Fehlern
  return NextResponse.json({ ... })
})
```

`withErrorHandler` übernimmt: Request-ID-Generierung, Logging (mit
Sensitive-Key-Redaction), Dauer-Messung, JSON-Parse-Fehler, und das Mapping von
`AppError` auf HTTP-Responses.

---

## 2. Fehler-Klassen & Response-Shape (CRITICAL — Lessons Learned)

Aus `lib/api-handler.ts`. Jede Klasse hat eine feste `userMessage` (öffentlich)
getrennt von der `message` (intern/Debug):

| Klasse | Status | userMessage (öffentlich) |
|--------|--------|--------------------------|
| `ValidationError` | 400 | frei wählbar (default = message) |
| `AuthenticationError` | 401 | **immer** `"Nicht authentifiziert"` |
| `ForbiddenError` | 403 | `"Keine Berechtigung"` |
| `NotFoundError` | 404 | `"Nicht gefunden"` |
| `ConflictError` | 409 | = message |
| `ExternalServiceError` | 502 | `"Externer Dienst nicht erreichbar"` |

**Response-Shape:** `{ error: userMessage, code, requestId }`. Wenn
`message !== userMessage` und `NODE_ENV !== 'production'`, wird zusätzlich
`detail: message` gesetzt.

**Diagnose-Regel (wichtig):** Bei jedem `"Nicht authentifiziert"` zuerst das Feld
`detail` der API-Response prüfen, nicht `error`. Die echte Ursache (z.B.
"Kein App-Benutzer", "Ungültige Anmeldedaten") steht in `detail`, weil
`AuthenticationError` die userMessage hart auf "Nicht authentifiziert" mappt.
Siehe KNOWLEDGE-Ticket #1720 und #1715.

---

## 3. Supabase-Clients (CRITICAL)

Aus `lib/supabase/server.ts`. Drei Clients, alle gegen
`NEXT_PUBLIC_SUPABASE_URL` (Self-Hosted Test-Instanz `api-sdr-test.senity.ai`):

| Funktion | Key | Zweck |
|----------|-----|-------|
| `createClient()` | ANON_KEY | Cookie-basierte SSR-Session (GoTrue), respektiert RLS |
| `createScopedClient()` | ANON_KEY + Bearer | Wie createClient, aber mit Access-Token-Header |
| `createAdminClient()` | SERVICE_ROLE_KEY | Service-Rolle, umgeht RLS, für privilegierte Reads/Writes |

**Wichtig:** `createAdminClient()` nutzt ebenfalls `NEXT_PUBLIC_SUPABASE_URL`,
**nicht** die Env-Variable `SUPABASE_URL` (`api-sdr.senity.ai`). Letztere wird vom
Login-Flow nicht verwendet. Die Workspace-Self-Hosted-Keys haben keinen
`ref`-Claim (nur `role`/`iss`/`iat`/`exp`).

---

## 4. Auth-Datenmodell (1:1 SDRv4)

Login (`app/api/auth/login/route.ts`) braucht **beides**:

1. Einen GoTrue-Auth-User (`signInWithPassword`).
2. Eine passende `app_user`-Zeile (`.eq('auth_user_id', authData.user.id)`),
   gejoint auf `contact`, plus Rollen aus `app_user_role` → `user_role`.

Fehlt der `app_user`, wird ausgeloggt (Ghost-Session-Schutz) und
`AuthenticationError('Kein App-Benutzer für dieses Konto gefunden')` geworfen.

**Spalten-Gotcha:** Das `app_user`-Select nutzt `ui_preferences` (existiert),
**nicht** `ai_preferences` (existiert nicht → PostgREST 42703). Ein kaputtes
Select setzt den `error`, und der Branch `if (appUserError || !appUser)` feuert
dann fälschlich "Kein App-Benutzer", obwohl die Zeile existiert. Siehe #1720.

**gmail/googlemail-Fallback:** GoTrue behandelt `@gmail.com` und
`@googlemail.com` als getrennte Konten. Die Login-Route probiert beide
Domain-Varianten durch (Kandidaten-Loop). Ein Auth-User kann existieren, ohne
einen `app_user` zu haben → dann korrekt "Kein App-Benutzer".

---

## 5. Logging & Secrets

- Logger redactet automatisch sensible Keys (`password`, `token`, `secret`,
  `authorization`, `cookie`, `api_key`, ...). Trotzdem niemals bewusst Credentials
  loggen.
- Secrets bleiben in `.env.local` (gitignored). Keine Keys/Tokens in getrackten
  Dateien. Vor jedem Commit `git diff --cached` visuell prüfen.
- Best-Effort-Logging (z.B. `login_event`) darf den Hauptpfad nie blockieren →
  in `try/catch` mit `console.warn` kapseln.

---

## 6. Env-Variablen (Login-relevant)

| Variable | Bedeutung |
|----------|-----------|
| `NEXT_PUBLIC_SUPABASE_URL` | `https://api-sdr-test.senity.ai` (Auth + Admin + Public) |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Public/Anon-Key |
| `SUPABASE_SERVICE_ROLE_KEY` | Service-Rolle für `createAdminClient()` |
| `SUPABASE_URL` | `api-sdr.senity.ai` — vom Login-Flow **nicht** genutzt |

---

## 7. Apps-System (modulare Features, aus SDRv4 portiert)

Das Mini-SDR spiegelt SDRv4s modulares `apps/`-System. Jede Feature-App ist ein
Paket unter `apps/<id>/` mit einer `manifest.json`. Die Kette ist 1:1 SDRv4
(FEAT-279 / FEAT-284), **ohne** den SDRv4-Produkt-Layer für Lizenz und
Container-Entitlement (im Workspace bewusst weggelassen).

**Kern-Kette:**

| Schritt | Datei | Zweck |
|---------|-------|-------|
| Manifest-Typ + Guard | `lib/apps/manifest.ts` | `AppManifest`, `AppNavEntry`, `isAppManifest()` |
| Registry | `lib/apps/registry.ts` | `loadAppManifests()` liest `apps/<id>/manifest.json`, Topological-Sort über `dependencies.apps`; fehlender `apps/`-Ordner → leere Registry (ENOENT) |
| Installer | `lib/apps/installer.ts` | `installApp()` / `uninstallApp()`, schreibt `app_module`, führt `install.sql` / `uninstall.sql` via RPC aus |
| DB-Tabelle | `db/005_app_module.sql` | `app_module` (Status, pricing_tier, manifest_snapshot, dependencies) |
| Migrations-RPC | `exec_app_migration(p_app_id, p_kind, p_sql)` | `SECURITY DEFINER`, nur `service_role`, validiert `p_kind IN (install, uninstall)` |
| Rollen-Gate | `lib/apps/require-role.ts` | `requireAnyRole()` statt SDRv4-Capability-System |
| Install/Uninstall-API | `app/api/apps/[id]/install/route.ts`, `.../uninstall/route.ts` | Thin-Re-Export-Muster, `withErrorHandler` |
| Nav-API | `app/api/nav/apps/route.ts` | liefert NavEntries installierter Apps für die dynamische Sidebar |

**Routing-Muster (Thin-Re-Export):** Die echte Logik liegt in
`apps/<id>/api/.../handlers.ts`; die Next-Route unter `app/api/...` re-exportiert
nur. So bleibt der App-Code im App-Paket.

**RouteHandler-Signatur-Gotcha:** Dynamische Routes müssen die `withErrorHandler`-
`RouteHandler`-Form treffen: `context?: { params: Promise<Record<string, string>> }`
(nicht `{ params: Promise<{ id: string }> }`). ID extrahieren via
`const { id } = (await context?.params) ?? {}`, dann `if (!id) throw new ValidationError(...)`.

**Typen-Gotcha:** Neue Tabellen/Funktionen (`app_module`, `exec_app_migration`)
müssen in `lib/supabase/types.ts` stehen, sonst liefert `.from('app_module')`
den Typ `never` bzw. die RPC hat untypisierte Args. SDRv4 generiert diese Typen;
im Workspace werden sie manuell gepflegt.

**Topologie-Warnung (CRITICAL):** Das Mini-SDR teilt sich die Supabase mit SDRv4
(`api-sdr-test.senity.ai`). `app_module` und `exec_app_migration` existieren dort
sehr wahrscheinlich bereits aus SDRv4-Migration 088. `db/005_app_module.sql` ist
idempotent (`IF NOT EXISTS`), aber vor Ausführung Topologie prüfen, nicht annehmen
es sei eine leere lokale DB.

**Bewusst weggelassen (SDRv4-Produkt-Layer):** Container-Scoping (`serverId`),
Lizenz-/Entitlement-Prüfung, Storage-/Cron-Cleanup beim Uninstall. Im Workspace
deaktiviert `uninstall` per Default nur (`status -> disabled`); erst mit
`cleanDatabase: true` läuft `uninstall.sql`.

---

## 8. Stil

- Antworten/Kommentare/Commits deutsch, echte Umlaute, **kein Em-Dash** (`—`).
- Destruktive DB-Operationen (DROP, TRUNCATE, DELETE ohne WHERE) brauchen
  explizite Bestätigung pro Aktion und vorherige Topologie-Prüfung (Ziel-DB
  verifizieren, nicht annehmen es sei lokal).
- Gelöste nicht-triviale Backend-Bugs als KNOWLEDGE-Ticket festhalten
  (`type_code=KNOWLEDGE`, `status_code=open`).
