# NexStream Catalog Backend Architecture (2026-06-01)

## Overview

The catalog system is split into two parts:

### 1. Backend: `services/catalog-worker/`
A TypeScript Cloudflare Worker that serves as the single source of truth for all catalog definitions and acts as an API proxy. API keys live server-side in Worker secrets — the APK never contains raw provider keys.

**Key files:**
- `src/index.ts` — Hono router, routes for catalogs, preferences, keys, health
- `src/core/catalogs.ts` — 92 catalog definitions from AIOMetadata blueprint
- `src/core/tmdb.ts` — TMDB proxy (trending, popular, discover)
- `src/core/mdblist.ts` — MDBList list proxy
- `src/core/keyRing.ts` — Encrypted user API key overrides
- `src/core/models.ts` — TypeScript types
- `wrangler.toml` — CF Worker config with all secrets

**Routes:**
- `GET /api/catalogs` — all catalog definitions
- `GET /api/catalog/:source/:id` — paginated catalog items
- `GET/POST /api/preferences/:deviceId` — per-device catalog preferences
- `GET/POST/DELETE /api/keys/:deviceId/:provider` — user API key overrides
- `GET /api/health` — per-source health status

### 2. Android Data Layer: `app/src/.../data/catalog/`

**Files added (2026-06-01):**
| File | Purpose |
|------|---------|
| `CatalogBackendApi.kt` | Retrofit interface for catalog-worker |
| `CatalogModels.kt` | `CatalogDefinition`, `CatalogResponse`, `CatalogPreferences`, etc. |
| `CatalogBackendRepository.kt` | Repository with in-memory cache + CompletableDeferred dedup |
| `CatalogLocalStore.kt` | Room entity + DAO for local catalog preferences |
| `EncryptedKeyStore.kt` | Android Keystore encrypted API key storage |
| `CatalogBackendModule.kt` | Hilt DI providing `CatalogBackendApi` |

**Key design:**
- Device ID from `Settings.Secure.ANDROID_ID`
- In-flight request dedup via `ConcurrentHashMap<String, CompletableDeferred<T>>`
- API keys masked in logs: first 4 + `****` + last 4
- Catalog preferences synced to backend + cached locally in Room
- Backend URL: `BuildConfig.CATALOG_BACKEND_URL` (default `http://10.0.2.2:3090/api/`)

### 3. Android UI: `app/src/.../ui/screens/catalog/`

**Files:**
| File | Purpose |
|------|---------|
| `CatalogManagerScreen.kt` | TV Compose screen with category tabs, enable/disable toggles |
| `CatalogManagerViewModel.kt` | HiltViewModel managing catalog state + preferences |

**Navigation:**
- Route: `Screen.CatalogManager` → `"settings/catalog-manager"`
- Settings entry: `SettingsPanel.CATALOGS` (between Addons and Playback)
- Embedded wrapper: `CatalogManagerEmbeddedScreen` for settings detail pane

**UI features:**
- 8 category tabs: Recommended, Movies, Shows, Anime, Kids, Genres, Streaming, Advanced
- Per-catalog enable/disable + show-in-home toggle
- Health banner showing source status
- "Reset Recommended" and "Reset All" actions
- API key override dialog (power users)

## Build Config

In `app/build.gradle.kts` defaultConfig:
```kotlin
val catalogBackendUrl: String = readBuildSecret("CATALOG_BACKEND_URL")
    .ifBlank { "http://10.0.2.2:3090/api/" }
buildConfigField("String", "CATALOG_BACKEND_URL", "\"${catalogBackendUrl}\"")
```

## Database Migrations

Added `CatalogPreferenceEntity` to Room → version bump to 9 with `MIGRATION_8_9`:
```sql
CREATE TABLE IF NOT EXISTS catalog_preferences (
    catalogId TEXT NOT NULL PRIMARY KEY,
    enabled INTEGER,
    showInHome INTEGER,
    customName TEXT,
    sortOrder INTEGER NOT NULL DEFAULT 0
)
```

## Pitfalls

- **Dagger DuplicateBindings**: Only `LocalDataModule` provides DAOs. `CatalogBackendModule` provides only the Retrofit API, not the DAO.
- **KSP + Unicode**: Box-drawing chars in comments break KSP. Use ASCII-only.
- **collectAsStateWithLifecycle**: Import from `androidx.lifecycle.compose.collectAsStateWithLifecycle`, NOT `androidx.compose.runtime.collectAsState`.
- **NuvioTheme.colors**: Use `Accent` (not `AccentBlue`), `Error` (not `ErrorRed`).
